From 81cf4d255e0ff7332eb4fc29d59c1e36e9f938a3 Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Wed, 12 Feb 2025 10:24:32 -0600 Subject: [PATCH 001/449] Matter Switch: move embedded cluster includes to top of file (#1925) These require statements are needed before the first reference of these clusters in the file, so they are moved to the top. --- drivers/SmartThings/matter-switch/src/init.lua | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b8fa4f6631..f6c17ce9a7 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -20,6 +20,14 @@ local lua_socket = require "socket" local utils = require "st.utils" local device_lib = require "st.device" local im = require "st.matter.interaction_model" +local embedded_cluster_utils = require "embedded-cluster-utils" +-- Include driver-side definitions when lua libs api version is < 11 +local version = require "version" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" +end local MOST_RECENT_TEMP = "mostRecentTemp" local RECEIVED_X = "receivedX" @@ -178,16 +186,6 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt -local embedded_cluster_utils = require "embedded-cluster-utils" - --- Include driver-side definitions when lua libs api version is < 11 -local version = require "version" -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" -end - -- Return an ISO-8061 timestamp in UTC local function iso8061Timestamp(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) From e0d915a0c3dfc2ed7b1582b5fa1a95c8aeb23a5d Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:13:23 -0600 Subject: [PATCH 002/449] Persist BooleanState Device Type to Endpoint Fields (#1919) * persist dt-ep mapping field, add dt-ep mapping read to init(), remove added handling --- drivers/SmartThings/matter-sensor/src/init.lua | 12 ++++-------- .../src/test/test_matter_freeze_leak_sensor.lua | 5 ++--- .../src/test/test_matter_rain_sensor.lua | 3 +-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 78b4c192b4..f8a36fa722 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -87,7 +87,7 @@ local function set_boolean_device_type_per_endpoint(driver, device) for _, dt in ipairs(ep.device_types) do for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do if dt.device_type_id == info.id then - device:set_field(dt_name, ep.endpoint_id) + device:set_field(dt_name, ep.endpoint_id, { persist = true }) device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) end end @@ -111,10 +111,6 @@ local function supports_sensitivity_preferences(device) return preference_names end -local function device_added(driver, device) - set_boolean_device_type_per_endpoint(driver, device) -end - local function match_profile(driver, device, battery_supported) local profile_name = "" @@ -188,13 +184,14 @@ end local function device_init(driver, device) log.info("device init") + set_boolean_device_type_per_endpoint(driver, device) device:subscribe() end local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() set_boolean_device_type_per_endpoint(driver, device) + device:subscribe() end if not device.preferences then return @@ -306,7 +303,7 @@ local function supported_sensitivities_handler(driver, device, ib, response) for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do if device:get_field(dt_name) == ib.endpoint_id then - device:set_field(info.sensitivity_max, ib.data.value) + device:set_field(info.sensitivity_max, ib.data.value, {persist = true}) end end end @@ -367,7 +364,6 @@ local matter_driver_template = { init = device_init, infoChanged = info_changed, doConfigure = do_configure, - added = device_added, }, matter_handlers = { attr = { 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 49c846f4ec..46bc88a771 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 @@ -69,11 +69,10 @@ local function test_init_freeze_leak() subscribe_request:merge(cluster:subscribe(mock_device_freeze_leak)) end end - test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_freeze_leak) - test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "added" }) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 1)}) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 2)}) + test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_freeze_leak) end test.set_test_init_function(test_init_freeze_leak) 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 3225a53e06..b23f72de53 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 @@ -61,10 +61,9 @@ local function test_init_rain() subscribe_request:merge(cluster:subscribe(mock_device_rain)) end end + test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) test.socket.matter:__expect_send({mock_device_rain.id, subscribe_request}) test.mock_device.add_test_device(mock_device_rain) - test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "added" }) - test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) end test.set_test_init_function(test_init_rain) From a71d5e71a427c17cb920d7b6e97674d5e8057380 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Thu, 13 Feb 2025 14:43:36 -0600 Subject: [PATCH 003/449] fix(hue): Fix Mirek -> Kelvin conversion constant. This constant should be 100, but I resolved a conflict during a rebase incorrectly when prepping the original PR and it got set to 11 instead of 100. --- drivers/SmartThings/philips-hue/src/consts.lua | 2 +- .../src/test/spec/unit/utils_spec.lua | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/philips-hue/src/consts.lua b/drivers/SmartThings/philips-hue/src/consts.lua index 1c35d92726..1f59a90de7 100644 --- a/drivers/SmartThings/philips-hue/src/consts.lua +++ b/drivers/SmartThings/philips-hue/src/consts.lua @@ -6,6 +6,6 @@ Consts.DEFAULT_MAX_MIREK = 500 Consts.MIN_TEMP_KELVIN_COLOR_AMBIANCE = 2000 Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE = 2200 Consts.MAX_TEMP_KELVIN = 6500 -Consts.KELVIN_STEP_SIZE = 11 +Consts.KELVIN_STEP_SIZE = 100 return Consts diff --git a/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua b/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua index c0791df114..861723a6a7 100644 --- a/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua +++ b/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua @@ -1,16 +1,32 @@ describe("utility functions", function() + local Consts local Fields local HueDeviceTypes local st_utils local utils setup(function() + Consts = require "consts" Fields = require "fields" HueDeviceTypes = require "hue_device_types" utils = require "utils" st_utils = require "st.utils" end) + describe("that perform mirek to kelvin conversions behave properly:", function() + it("Common minimum mirek of 153 results in 6500 Kelvin", function() + local expected_kelvin = 6500 + local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MIN_MIREK) + assert.are.equal(expected_kelvin, computed_kelvin, string.format("Expected value of %s, got %s", expected_kelvin, computed_kelvin)) + end) + + it("Common maximum mirek of 500 results in 2000 Kelvin", function() + local expected_kelvin = 2000 + local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MAX_MIREK) + assert.are.equal(expected_kelvin, computed_kelvin, string.format("Expected value of %s, got %s", expected_kelvin, computed_kelvin)) + end) + end) + describe("that handle raw data will handle their input correctly:", function() local test_mac_addr local test_cisco_mac_addr From 982a9cefe2e25c3dede54e1dd9fa8aaa80bbee65 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Mon, 3 Mar 2025 16:47:49 -0600 Subject: [PATCH 004/449] fix: Echo user requested setpoint for kelvin If the bulb's color temperature changes because of an ST-initiated user requested setpoint, we emit that directly instead of clamping to the step size. This is to remain consistent with the way things are done on the rest of the platform with other integrations. --- drivers/SmartThings/philips-hue/src/fields.lua | 1 + .../philips-hue/src/handlers/attribute_emitters.lua | 9 +++++++++ .../SmartThings/philips-hue/src/handlers/commands.lua | 1 + 3 files changed, 11 insertions(+) diff --git a/drivers/SmartThings/philips-hue/src/fields.lua b/drivers/SmartThings/philips-hue/src/fields.lua index 6a02307f53..5091354d21 100644 --- a/drivers/SmartThings/philips-hue/src/fields.lua +++ b/drivers/SmartThings/philips-hue/src/fields.lua @@ -33,6 +33,7 @@ local Fields = { COLOR_SATURATION = "color_saturation", COLOR_HUE = "color_hue", SWITCH_STATE = "switch_state_cache", + COLOR_TEMP_SETPOINT = "ct_setpoint" } return Fields diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index c98a047b1f..854e522fb1 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -101,8 +101,17 @@ local function _emit_light_events_inner(light_device, light_repr) ) ) else + local last_kelvin_setpoint = light_device:get_field(Fields.COLOR_TEMP_SETPOINT) + if + last_kelvin_setpoint ~= nil and + last_kelvin_setpoint >= utils.mirek_to_kelvin(mirek + 1) and + last_kelvin_setpoint <= utils.mirek_to_kelvin(mirek - 1) + then + kelvin = last_kelvin_setpoint; + end light_device:emit_event(capabilities.colorTemperature.colorTemperature(kelvin)) end + light_device:set_field(Fields.COLOR_TEMP_SETPOINT, nil); end if light_repr.color then diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index a0cf161081..cf30834a2c 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -249,6 +249,7 @@ local function do_color_temp_action(driver, device, args) end end end + device:set_field(Fields.COLOR_TEMP_SETPOINT, clamped_kelvin); end ---@param driver HueDriver From e7348554c15d4f0cb5e0d0a616f363a2c9ad3568 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Mon, 3 Mar 2025 22:23:08 -0600 Subject: [PATCH 005/449] fix: Address bug in color temp range emit logic The existing logic utilized the device persistent fields to determine if a range update was needed, as to avoid un-necessary attribute emits. This would lead to existing devices not ever emitting the min/max color temp range values if the saved min/max matched what was reported by the API. This is adjusted to use the cached latest state for the device instead, so that we can detect the `nil` if the device has never emitted a value for its range. --- .../philips-hue/src/handlers/attribute_emitters.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index 854e522fb1..e353e04ce6 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -67,9 +67,10 @@ local function _emit_light_events_inner(light_device, light_repr) -- See note in `src/handlers/lifecycle_handlers/light.lua` about min/max relationship -- if the below is not intuitive. - local min_kelvin = light_device:get_field(Fields.MIN_KELVIN) + local color_temp_range = light_device:get_latest_state("main", capabilities.colorTemperature.ID, capabilities.colorTemperature.colorTemperatureRange.NAME); + local min_kelvin = (color_temp_range and color_temp_range.minimum) local api_min_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_maximum) or Consts.MIN_TEMP_KELVIN_COLOR_AMBIANCE) - local max_kelvin = light_device:get_field(Fields.MAX_KELVIN) + local max_kelvin = (color_temp_range and color_temp_range.maximum) local api_max_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_minimum) or Consts.MAX_TEMP_KELVIN) local update_range = false @@ -86,7 +87,10 @@ local function _emit_light_events_inner(light_device, light_repr) end if update_range then + light_device.log.debug(st_utils.stringify_table({ minimum = min_kelvin, maximum = max_kelvin }, "updating color temp range")); light_device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ minimum = min_kelvin, maximum = max_kelvin })) + else + light_device.log.debug(st_utils.stringify_table(color_temp_range, "color temp range unchanged")); end -- local min = or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE From f4677b0e76fa285dda7429cde73bf94dc0918b6d Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 5 Mar 2025 13:14:26 -0600 Subject: [PATCH 006/449] fix(hue): Make rounding logic platform consistent There was a slight bug with the previous port of the rounding logic from Matter/Zigbee over to Hue because I was reading two different places in the code and getting them confused in my head. This change does the following: 1. Only use the 100 step size constraint when computing the min/max values instead of everywhere. This is in line with how this is handled elsewhere on the platform. 2. Use normal rounding everywhere else. --- .../philips-hue/src/handlers/attribute_emitters.lua | 4 ++-- .../src/handlers/lifecycle_handlers/light.lua | 4 ++-- .../philips-hue/src/test/spec/unit/utils_spec.lua | 4 ++-- drivers/SmartThings/philips-hue/src/utils/init.lua | 11 ++++++++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index e353e04ce6..736d79af73 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -69,9 +69,9 @@ local function _emit_light_events_inner(light_device, light_repr) -- if the below is not intuitive. local color_temp_range = light_device:get_latest_state("main", capabilities.colorTemperature.ID, capabilities.colorTemperature.colorTemperatureRange.NAME); local min_kelvin = (color_temp_range and color_temp_range.minimum) - local api_min_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_maximum) or Consts.MIN_TEMP_KELVIN_COLOR_AMBIANCE) + local api_min_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_maximum, Consts.KELVIN_STEP_SIZE) or Consts.MIN_TEMP_KELVIN_COLOR_AMBIANCE) local max_kelvin = (color_temp_range and color_temp_range.maximum) - local api_max_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_minimum) or Consts.MAX_TEMP_KELVIN) + local api_max_kelvin = math.floor(utils.mirek_to_kelvin(mirek_schema.mirek_minimum, Consts.KELVIN_STEP_SIZE) or Consts.MAX_TEMP_KELVIN) local update_range = false if min_kelvin ~= api_min_kelvin then diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua index d0f9869ca9..64b1bcfa84 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua @@ -158,10 +158,10 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource if caps.colorTemperature then local min_ct_kelvin, max_ct_kelvin = nil, nil if type(light_info.color_temperature.mirek_schema.mirek_maximum) == "number" then - min_ct_kelvin = math.floor(utils.mirek_to_kelvin(light_info.color_temperature.mirek_schema.mirek_maximum)) + min_ct_kelvin = math.floor(utils.mirek_to_kelvin(light_info.color_temperature.mirek_schema.mirek_maximum, Consts.KELVIN_STEP_SIZE)) end if type(light_info.color_temperature.mirek_schema.mirek_minimum) == "number" then - max_ct_kelvin = math.floor(utils.mirek_to_kelvin(light_info.color_temperature.mirek_schema.mirek_minimum)) + max_ct_kelvin = math.floor(utils.mirek_to_kelvin(light_info.color_temperature.mirek_schema.mirek_minimum, Consts.KELVIN_STEP_SIZE)) end if not min_ct_kelvin then if caps.colorControl then diff --git a/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua b/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua index 861723a6a7..724d7316cf 100644 --- a/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua +++ b/drivers/SmartThings/philips-hue/src/test/spec/unit/utils_spec.lua @@ -16,13 +16,13 @@ describe("utility functions", function() describe("that perform mirek to kelvin conversions behave properly:", function() it("Common minimum mirek of 153 results in 6500 Kelvin", function() local expected_kelvin = 6500 - local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MIN_MIREK) + local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MIN_MIREK, Consts.KELVIN_STEP_SIZE) assert.are.equal(expected_kelvin, computed_kelvin, string.format("Expected value of %s, got %s", expected_kelvin, computed_kelvin)) end) it("Common maximum mirek of 500 results in 2000 Kelvin", function() local expected_kelvin = 2000 - local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MAX_MIREK) + local computed_kelvin = utils.mirek_to_kelvin(Consts.DEFAULT_MAX_MIREK, Consts.KELVIN_STEP_SIZE) assert.are.equal(expected_kelvin, computed_kelvin, string.format("Expected value of %s, got %s", expected_kelvin, computed_kelvin)) end) end) diff --git a/drivers/SmartThings/philips-hue/src/utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua index b45184e54d..c52ee10c28 100644 --- a/drivers/SmartThings/philips-hue/src/utils/init.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -1,6 +1,6 @@ local log = require "log" +local st_utils = require "st.utils" -local Consts = require "consts" local Fields = require "fields" local HueDeviceTypes = require "hue_device_types" @@ -72,9 +72,14 @@ end function utils.kelvin_to_mirek(kelvin) return 1000000 / kelvin end -function utils.mirek_to_kelvin(mirek) +function utils.mirek_to_kelvin(mirek, with_step_size) local raw_kelvin = 1000000 / mirek - return Consts.KELVIN_STEP_SIZE * math.floor(raw_kelvin / Consts.KELVIN_STEP_SIZE) + + if type(with_step_size) == "number" then + return with_step_size * math.floor(raw_kelvin / with_step_size) + end + + return st_utils.round(raw_kelvin) end function utils.str_starts_with(str, start) From 97d567656b639715e798179d91658dd8888d6373 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Mon, 24 Mar 2025 17:13:36 -0500 Subject: [PATCH 007/449] feat(sonos): Add reporting for Sonos App `swGen` Adds support for the `stus` custom capability `softwareGeneration`, which allow for reporting of the `swGen` field on the Sonos Player Info. This field reports whether or not the speaker has been onboarded to Sonos via the S1 app or the modern Sonos app. --- .../capabilities/softwareGeneration.yaml | 17 +++++++++++ .../sonos/profiles/sonos-player.yml | 2 ++ drivers/SmartThings/sonos/src/disco.lua | 8 ++--- drivers/SmartThings/sonos/src/fields.lua | 1 + drivers/SmartThings/sonos/src/init.lua | 29 ++++++++++++++----- drivers/SmartThings/sonos/src/types.lua | 10 ++++++- 6 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 drivers/SmartThings/sonos/capabilities/softwareGeneration.yaml diff --git a/drivers/SmartThings/sonos/capabilities/softwareGeneration.yaml b/drivers/SmartThings/sonos/capabilities/softwareGeneration.yaml new file mode 100644 index 0000000000..8a0a9229a4 --- /dev/null +++ b/drivers/SmartThings/sonos/capabilities/softwareGeneration.yaml @@ -0,0 +1,17 @@ +id: stus.softwareGeneration +version: 1 +status: proposed +name: Software Generation +ephemeral: false +attributes: + generation: + schema: + type: object + properties: + value: + type: string + additionalProperties: false + required: + - value + enumCommands: [] +commands: {} diff --git a/drivers/SmartThings/sonos/profiles/sonos-player.yml b/drivers/SmartThings/sonos/profiles/sonos-player.yml index c7fd45c238..a8030db88c 100644 --- a/drivers/SmartThings/sonos/profiles/sonos-player.yml +++ b/drivers/SmartThings/sonos/profiles/sonos-player.yml @@ -30,6 +30,8 @@ components: version: 1 - id: audioVolume version: 1 + - id: stus.softwareGeneration + version: 1 - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/sonos/src/disco.lua b/drivers/SmartThings/sonos/src/disco.lua index 7a9b42fc51..07b4d73d65 100644 --- a/drivers/SmartThings/sonos/src/disco.lua +++ b/drivers/SmartThings/sonos/src/disco.lua @@ -13,19 +13,19 @@ local ssdp_discovery_callback = function(driver, ssdp_group_info, known_devices_ if not found_ip_addrs[ssdp_group_info.ip] then found_ip_addrs[ssdp_group_info.ip] = true + ---@type DiscoCallback local function add_device_callback(dni, inner_ssdp_group_info, player_info, group_info) if not known_devices_dnis[dni] then local name = player_info.device.name or player_info.device.modelDisplayName or "Unknown Sonos Player" local model = player_info.device.modelDisplayName or "Unknown Sonos Model" - local field_cache = { + driver._field_cache[dni] = { household_id = inner_ssdp_group_info.household_id, player_id = player_info.playerId, - wss_url = player_info.websocketUrl + wss_url = player_info.websocketUrl, + swGen = player_info.device.swGen } - driver._field_cache[dni] = field_cache - driver.sonos:update_household_info(player_info.householdId, group_info) local create_device_msg = { diff --git a/drivers/SmartThings/sonos/src/fields.lua b/drivers/SmartThings/sonos/src/fields.lua index 6bf880033f..7827654dc6 100644 --- a/drivers/SmartThings/sonos/src/fields.lua +++ b/drivers/SmartThings/sonos/src/fields.lua @@ -9,6 +9,7 @@ Fields.SonosPlayerFields = { HOUSEHOULD_ID = "householdId", PLAYER_ID = "playerId", WSS_URL = "wss_url", + SW_GEN = "sw_gen", } return Fields diff --git a/drivers/SmartThings/sonos/src/init.lua b/drivers/SmartThings/sonos/src/init.lua index 4b338c44f4..31fc7d41a9 100644 --- a/drivers/SmartThings/sonos/src/init.lua +++ b/drivers/SmartThings/sonos/src/init.lua @@ -38,6 +38,8 @@ local SonosState = require "types".SonosState local SSDP = require "ssdp" local utils = require "utils" +local swGenCapability = capabilities["stus.softwareGeneration"] + --- @param driver SonosDriver --- @param device SonosDevice --- @param should_continue function|nil @@ -65,14 +67,13 @@ local function find_player_for_device(driver, device, should_continue) if dni_equal(dni, device.device_network_id) then device.log.info(string.format("Found Sonos Player match for device")) - local field_cache = { + driver._field_cache[dni] = { household_id = inner_ssdp_group_info.household_id, player_id = player_info.playerId, - wss_url = player_info.websocketUrl + wss_url = player_info.websocketUrl, + swGen = player_info.device.swGen } - driver._field_cache[dni] = field_cache - driver.sonos:update_household_info(player_info.householdId, group_info) player_found = true end @@ -83,17 +84,21 @@ local function find_player_for_device(driver, device, should_continue) return player_found end +---@param driver SonosDriver +---@param device SonosDevice +---@param fields SonosFieldCacheTable local function update_fields_from_ssdp_scan(driver, device, fields) device.log.debug("Updating fields from SSDP scan") local is_initialized = device:get_field(PlayerFields._IS_INIT) - local current_player_id, current_url, current_household_id, sonos_conn + local current_player_id, current_url, current_household_id, sonos_conn, sw_gen if is_initialized then current_player_id =device:get_field(PlayerFields.PLAYER_ID) current_url = device:get_field(PlayerFields.WSS_URL) current_household_id = device:get_field(PlayerFields.HOUSEHOULD_ID) sonos_conn = device:get_field(PlayerFields.CONNECTION) + sw_gen = device:get_field(PlayerFields.SW_GEN) end local already_connected = sonos_conn ~= nil @@ -127,6 +132,11 @@ local function update_fields_from_ssdp_scan(driver, device, fields) device:set_field(PlayerFields.WSS_URL, fields.wss_url, { persist = true }) end + if sw_gen ~= fields.swGen then + device:set_field(PlayerFields.SW_GEN, fields.swGen, {persist = true}) + device:emit_event(swGenCapability.generation(string.format("%s", fields.swGen))) + end + if refresh and already_connected then if not sonos_conn then sonos_conn = SonosConnection.new(driver, device) @@ -140,14 +150,19 @@ local function update_fields_from_ssdp_scan(driver, device, fields) end end + +---@param driver SonosDriver local function scan_for_ssdp_updates(driver) SSDP.search(SONOS_SSDP_SEARCH_TERM, function(ssdp_group_info) driver:handle_ssdp_discovery(ssdp_group_info, function(dni, inner_ssdp_group_info, player_info, group_info) local current_cached_fields = driver._field_cache[dni] or {} + + ---@type SonosFieldCacheTable local updated_fields = { household_id = inner_ssdp_group_info.household_id, player_id = player_info.playerId, - wss_url = player_info.websocketUrl + wss_url = player_info.websocketUrl, + swGen = player_info.device.swGen } driver._field_cache[dni] = updated_fields @@ -315,7 +330,7 @@ end --- @param self SonosDriver --- @param ssdp_group_info SonosSSDPInfo ---- @param callback? fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfo, group_info: SonosGroupsResponseBody) +--- @param callback? DiscoCallback local function handle_ssdp_discovery(self, ssdp_group_info, callback) log.debug(string.format("Looking for player info for SSDP search results %s", st_utils.stringify_table(ssdp_group_info))) local player_info, err = SonosRestApi.get_player_info(ssdp_group_info.ip, SonosApi.DEFAULT_SONOS_PORT) diff --git a/drivers/SmartThings/sonos/src/types.lua b/drivers/SmartThings/sonos/src/types.lua index f729105acf..14f1c5b08e 100644 --- a/drivers/SmartThings/sonos/src/types.lua +++ b/drivers/SmartThings/sonos/src/types.lua @@ -380,10 +380,18 @@ Types.SonosState = SonosState --- @alias DiscoCallback fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfo, group_info: SonosGroupsResponseBody) +---@class SonosFieldCacheTable +---@field public swGen number +---@field public household_id string +---@field public player_id string +---@field public wss_url string + + --- Sonos Edge Driver extensions --- @class SonosDriver : Driver --- @field public sonos SonosState Local state related to the sonos systems ---- @field private _player_id_to_device table +--- @field public _player_id_to_device table +--- @field public _field_cache table --- @field public update_group_state fun(self: SonosDriver, header: SonosResponseHeader, body: SonosGroupsResponseBody) --- @field public handle_ssdp_discovery fun(self: SonosDriver, ssdp_group_info: SonosSSDPInfo, callback?: DiscoCallback) --- @field public is_same_mac_address fun(dni: string, other: string): boolean From 28d5398115bdefb18771ef685e46165bbca07b85 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 27 May 2025 15:31:59 -0500 Subject: [PATCH 008/449] Matter Thermostat: Include new fingerprint and profiles, update comments, update configure logic (#2131) --- .../matter-sensor/fingerprints.yml | 12 +++--- .../matter-thermostat/fingerprints.yml | 10 ++++- ...rature-humidity-fan-aqs-pm25-tvoc-meas.yml | 43 +++++++++++++++++++ ...n-nostate-nobattery-aqs-pm25-tvoc-meas.yml | 6 +++ .../air-purifier-temperature-humidity-aqs.yml | 20 +++++++++ .../air-purifier-thermostat-humidity-aqs.yml | 15 +++++++ ...rifier-thermostat-nostate-humidity-aqs.yml | 26 +++++++++++ .../matter-thermostat/src/init.lua | 10 +++++ 8 files changed, 135 insertions(+), 7 deletions(-) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-temperature-humidity-fan-aqs-pm25-tvoc-meas.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-temperature-humidity-aqs.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-nostate-humidity-aqs.yml diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index df44ea200d..65cd70826e 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -219,30 +219,30 @@ matterGeneric: - id: "matter/air-quality-sensor" deviceLabel: Matter Air Quality Sensor deviceTypes: - - id: 0x002C + - id: 0x002C # Air Quality Sensor deviceProfileName: aqs - id: "matter/smoke-co-alarm" deviceLabel: Matter Smoke/CO Alarm deviceTypes: - - id: 0x0076 + - id: 0x0076 # Smoke CO Alarm deviceProfileName: smoke-co - id: "matter/rain/sensor" deviceLabel: Matter Rain Sensor deviceTypes: - - id: 0x0044 + - id: 0x0044 # Rain Sensor deviceProfileName: rain-battery - id: "matter/freeze/detector" deviceLabel: Matter Water Freeze Detector deviceTypes: - - id: 0x0041 + - id: 0x0041 # Freeze Detector deviceProfileName: freeze-battery - id: "matter/leak/detector" deviceLabel: Matter Water Leak Detector deviceTypes: - - id: 0x0043 + - id: 0x0043 # Leak Sensor deviceProfileName: leak-battery - id: "matter/flow/sensor" deviceLabel: Matter Flow Sensor deviceTypes: - - id: 0x0306 + - id: 0x0306 # Flow Sensor deviceProfileName: flow-battery diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index e1dbe08bd7..ddeba331e0 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -127,7 +127,15 @@ matterGeneric: - id: 0x002C # Air Quality Sensor - id: 0x0301 # Thermostat - id: 0x0307 # Humidity Sensor - deviceProfileName: air-purifier-thermostat-humidity-aqs + deviceProfileName: air-purifier-thermostat-nostate-humidity-aqs + - id: "matter/ap/aqs/temperature/humidity" + deviceLabel: Matter Air Purifier & Quality Sensor + deviceTypes: + - id: 0x002D # Air Purifier + - id: 0x002C # Air Quality Sensor + - id: 0x0302 # Temperature Sensor + - id: 0x0307 # Humidity Sensor + deviceProfileName: air-purifier-temperature-humidity-aqs - id: "matter/water-heater" deviceLabel: Matter Water Heater deviceTypes: diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-temperature-humidity-fan-aqs-pm25-tvoc-meas.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-temperature-humidity-fan-aqs-pm25-tvoc-meas.yml new file mode 100644 index 0000000000..a234da4467 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-temperature-humidity-fan-aqs-pm25-tvoc-meas.yml @@ -0,0 +1,43 @@ +name: air-purifier-hepa-ac-temperature-humidity-fan-aqs-pm25-tvoc-meas +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: temperatureMeasurement + version: 1 + - id: fineDustSensor + version: 1 + - id: tvocMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: airQualityHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa Filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier +- id: activatedCarbonFilter + label: Activated Carbon Filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-thermostat-humidity-fan-nostate-nobattery-aqs-pm25-tvoc-meas.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-thermostat-humidity-fan-nostate-nobattery-aqs-pm25-tvoc-meas.yml index 77c6632806..707806c317 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-thermostat-humidity-fan-nostate-nobattery-aqs-pm25-tvoc-meas.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-ac-thermostat-humidity-fan-nostate-nobattery-aqs-pm25-tvoc-meas.yml @@ -9,6 +9,12 @@ components: version: 1 - id: temperatureMeasurement version: 1 + - id: thermostatMode + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + - id: thermostatCoolingSetpoint + version: 1 - id: fineDustSensor version: 1 - id: tvocMeasurement diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-temperature-humidity-aqs.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-temperature-humidity-aqs.yml new file mode 100644 index 0000000000..7305fcfc45 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-temperature-humidity-aqs.yml @@ -0,0 +1,20 @@ +name: air-purifier-temperature-humidity-aqs +components: +- id: main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: airQualityHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-humidity-aqs.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-humidity-aqs.yml index 627be0a3ca..58e285e33b 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-humidity-aqs.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-humidity-aqs.yml @@ -8,6 +8,21 @@ components: version: 1 - id: temperatureMeasurement version: 1 + - id: thermostatMode + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + - id: thermostatCoolingSetpoint + version: 1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - idle + - cooling + - heating - id: relativeHumidityMeasurement version: 1 - id: airQualityHealthConcern diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-nostate-humidity-aqs.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-nostate-humidity-aqs.yml new file mode 100644 index 0000000000..0e4cd24f7c --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-thermostat-nostate-humidity-aqs.yml @@ -0,0 +1,26 @@ +name: air-purifier-thermostat-nostate-humidity-aqs +components: +- id: main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: temperatureMeasurement + version: 1 + - id: thermostatMode + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + - id: thermostatCoolingSetpoint + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: airQualityHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 717d5fabbb..3b11d3c60e 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -773,6 +773,16 @@ local function match_profile_switch(driver, device) elseif battery_supported == battery_support.NO_BATTERY then profile_name = profile_name .. "-nobattery" end + elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then + profile_name = profile_name .. "-temperature" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end end profile_name = profile_name .. create_air_quality_sensor_profile(device) elseif device_type == WATER_HEATER_DEVICE_TYPE_ID then From d93e8072e0a23847c3274798a54ee38508f988ea Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Wed, 28 May 2025 22:21:01 +0900 Subject: [PATCH 009/449] Add service area feature to matter-rvc driver (#2120) Signed-off-by: Hunsup Jung Co-authored-by: Priyansh --- .../profiles/rvc-clean-mode-service-area.yml | 125 ++++++ .../matter-rvc/profiles/rvc-service-area.yml | 81 ++++ .../matter-rvc/src/Global/init.lua | 26 ++ .../src/Global/types/AreaTypeTag.lua | 423 ++++++++++++++++++ .../src/Global/types/LandmarkTag.lua | 247 ++++++++++ .../Global/types/LocationDescriptorStruct.lua | 106 +++++ .../src/Global/types/RelativePositionTag.lua | 71 +++ .../matter-rvc/src/Global/types/init.lua | 36 ++ .../client/commands/SelectAreasResponse.lua | 150 +++++++ .../src/ServiceArea/client/commands/init.lua | 42 ++ .../matter-rvc/src/ServiceArea/init.lua | 183 ++++++++ .../server/attributes/SelectedAreas.lua | 126 ++++++ .../server/attributes/SupportedAreas.lua | 126 ++++++ .../ServiceArea/server/attributes/init.lua | 44 ++ .../server/commands/SelectAreas.lua | 127 ++++++ .../src/ServiceArea/server/commands/init.lua | 42 ++ .../src/ServiceArea/server/events/init.lua | 42 ++ .../src/ServiceArea/types/AreaInfoStruct.lua | 98 ++++ .../src/ServiceArea/types/AreaStruct.lua | 106 +++++ .../src/ServiceArea/types/Feature.lua | 137 ++++++ .../ServiceArea/types/LandmarkInfoStruct.lua | 98 ++++ .../ServiceArea/types/SelectAreasStatus.lua | 59 +++ .../matter-rvc/src/ServiceArea/types/init.lua | 36 ++ .../matter-rvc/src/embedded_cluster_utils.lua | 49 ++ drivers/SmartThings/matter-rvc/src/init.lua | 134 +++++- .../matter-rvc/src/test/test_matter_rvc.lua | 177 +++++++- 26 files changed, 2883 insertions(+), 8 deletions(-) create mode 100644 drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml create mode 100644 drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml create mode 100644 drivers/SmartThings/matter-rvc/src/Global/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/Global/types/AreaTypeTag.lua create mode 100644 drivers/SmartThings/matter-rvc/src/Global/types/LandmarkTag.lua create mode 100644 drivers/SmartThings/matter-rvc/src/Global/types/LocationDescriptorStruct.lua create mode 100644 drivers/SmartThings/matter-rvc/src/Global/types/RelativePositionTag.lua create mode 100644 drivers/SmartThings/matter-rvc/src/Global/types/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/SelectAreasResponse.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SelectedAreas.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SupportedAreas.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/SelectAreas.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/server/events/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaInfoStruct.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaStruct.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/Feature.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/LandmarkInfoStruct.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/SelectAreasStatus.lua create mode 100644 drivers/SmartThings/matter-rvc/src/ServiceArea/types/init.lua create mode 100644 drivers/SmartThings/matter-rvc/src/embedded_cluster_utils.lua diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml new file mode 100644 index 0000000000..d27917e8af --- /dev/null +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -0,0 +1,125 @@ +name: rvc-clean-mode-service-area +components: + - id: main + label: Main + capabilities: + - id: robotCleanerOperatingState + version: 1 + - id: serviceArea + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: RobotCleaner + - id: runMode + label: Run mode + capabilities: + - id: mode + version: 1 + categories: + - name: RobotCleaner + - id: cleanMode + label: Clean mode + capabilities: + - id: mode + version: 1 + categories: + - name: RobotCleaner +deviceConfig: + dashboard: + states: + - component: main + capability: robotCleanerOperatingState + version: 1 + detailView: + - component: main + capability: robotCleanerOperatingState + version: 1 + - component: main + capability: serviceArea + version: 1 + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/list/command/supportedValues + value: supportedArguments.value + - component: cleanMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/list/command/supportedValues + value: supportedArguments.value + - component: main + capability: refresh + version: 1 + - component: main + capability: firmwareUpdate + version: 1 + automation: + conditions: + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + value: mode.value + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list + - component: cleanMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + value: mode.value + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list + actions: + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + command: setMode + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list + - component: cleanMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + command: setMode + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml new file mode 100644 index 0000000000..8b992c5ace --- /dev/null +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml @@ -0,0 +1,81 @@ +name: rvc-service-area +components: + - id: main + label: Main + capabilities: + - id: robotCleanerOperatingState + version: 1 + - id: serviceArea + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: RobotCleaner + - id: runMode + label: Run mode + capabilities: + - id: mode + version: 1 + categories: + - name: RobotCleaner +deviceConfig: + dashboard: + states: + - component: main + capability: robotCleanerOperatingState + version: 1 + detailView: + - component: main + capability: robotCleanerOperatingState + version: 1 + - component: main + capability: serviceArea + version: 1 + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/list/command/supportedValues + value: supportedArguments.value + - component: main + capability: refresh + version: 1 + - component: main + capability: firmwareUpdate + version: 1 + automation: + conditions: + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + value: mode.value + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list + actions: + - component: runMode + capability: mode + version: 1 + patch: + - op: replace + path: /0/displayType + value: dynamicList + - op: add + path: /0/dynamicList + value: + command: setMode + supportedValues: + value: supportedModes.value + - op: remove + path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/src/Global/init.lua b/drivers/SmartThings/matter-rvc/src/Global/init.lua new file mode 100644 index 0000000000..97726555b4 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/init.lua @@ -0,0 +1,26 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local GlobalTypes = require "Global.types" + +--- @class Global +--- @alias Global +--- +--- @field public types GlobalTypes + +local Global = {} +Global.types = GlobalTypes +return Global diff --git a/drivers/SmartThings/matter-rvc/src/Global/types/AreaTypeTag.lua b/drivers/SmartThings/matter-rvc/src/Global/types/AreaTypeTag.lua new file mode 100644 index 0000000000..3e20ae1b8e --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/types/AreaTypeTag.lua @@ -0,0 +1,423 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class Global.types.AreaTypeTag: st.matter.data_types.Uint8 +--- @alias AreaTypeTag +--- +--- @field public byte_length number 1 +--- @field public AISLE number 0 +--- @field public ATTIC number 1 +--- @field public BACK_DOOR number 2 +--- @field public BACK_YARD number 3 +--- @field public BALCONY number 4 +--- @field public BALLROOM number 5 +--- @field public BATHROOM number 6 +--- @field public BEDROOM number 7 +--- @field public BORDER number 8 +--- @field public BOXROOM number 9 +--- @field public BREAKFAST_ROOM number 10 +--- @field public CARPORT number 11 +--- @field public CELLAR number 12 +--- @field public CLOAKROOM number 13 +--- @field public CLOSET number 14 +--- @field public CONSERVATORY number 15 +--- @field public CORRIDOR number 16 +--- @field public CRAFT_ROOM number 17 +--- @field public CUPBOARD number 18 +--- @field public DECK number 19 +--- @field public DEN number 20 +--- @field public DINING number 21 +--- @field public DRAWING_ROOM number 22 +--- @field public DRESSING_ROOM number 23 +--- @field public DRIVEWAY number 24 +--- @field public ELEVATOR number 25 +--- @field public ENSUITE number 26 +--- @field public ENTRANCE number 27 +--- @field public ENTRYWAY number 28 +--- @field public FAMILY_ROOM number 29 +--- @field public FOYER number 30 +--- @field public FRONT_DOOR number 31 +--- @field public FRONT_YARD number 32 +--- @field public GAME_ROOM number 33 +--- @field public GARAGE number 34 +--- @field public GARAGE_DOOR number 35 +--- @field public GARDEN number 36 +--- @field public GARDEN_DOOR number 37 +--- @field public GUEST_BATHROOM number 38 +--- @field public GUEST_BEDROOM number 39 +--- @field public GUEST_RESTROOM number 40 +--- @field public GUEST_ROOM number 41 +--- @field public GYM number 42 +--- @field public HALLWAY number 43 +--- @field public HEARTH_ROOM number 44 +--- @field public KIDS_ROOM number 45 +--- @field public KIDS_BEDROOM number 46 +--- @field public KITCHEN number 47 +--- @field public LARDER number 48 +--- @field public LAUNDRY_ROOM number 49 +--- @field public LAWN number 50 +--- @field public LIBRARY number 51 +--- @field public LIVING_ROOM number 52 +--- @field public LOUNGE number 53 +--- @field public MEDIA_TV_ROOM number 54 +--- @field public MUD_ROOM number 55 +--- @field public MUSIC_ROOM number 56 +--- @field public NURSERY number 57 +--- @field public OFFICE number 58 +--- @field public OUTDOOR_KITCHEN number 59 +--- @field public OUTSIDE number 60 +--- @field public PANTRY number 61 +--- @field public PARKING_LOT number 62 +--- @field public PARLOR number 63 +--- @field public PATIO number 64 +--- @field public PLAY_ROOM number 65 +--- @field public POOL_ROOM number 66 +--- @field public PORCH number 67 +--- @field public PRIMARY_BATHROOM number 68 +--- @field public PRIMARY_BEDROOM number 69 +--- @field public RAMP number 70 +--- @field public RECEPTION_ROOM number 71 +--- @field public RECREATION_ROOM number 72 +--- @field public RESTROOM number 73 +--- @field public ROOF number 74 +--- @field public SAUNA number 75 +--- @field public SCULLERY number 76 +--- @field public SEWING_ROOM number 77 +--- @field public SHED number 78 +--- @field public SIDE_DOOR number 79 +--- @field public SIDE_YARD number 80 +--- @field public SITTING_ROOM number 81 +--- @field public SNUG number 82 +--- @field public SPA number 83 +--- @field public STAIRCASE number 84 +--- @field public STEAM_ROOM number 85 +--- @field public STORAGE_ROOM number 86 +--- @field public STUDIO number 87 +--- @field public STUDY number 88 +--- @field public SUN_ROOM number 89 +--- @field public SWIMMING_POOL number 90 +--- @field public TERRACE number 91 +--- @field public UTILITY_ROOM number 92 +--- @field public WARD number 93 +--- @field public WORKSHOP number 94 + +local AreaTypeTag = {} +local new_mt = UintABC.new_mt({NAME = "AreaTypeTag", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.AISLE] = "AISLE", + [self.ATTIC] = "ATTIC", + [self.BACK_DOOR] = "BACK_DOOR", + [self.BACK_YARD] = "BACK_YARD", + [self.BALCONY] = "BALCONY", + [self.BALLROOM] = "BALLROOM", + [self.BATHROOM] = "BATHROOM", + [self.BEDROOM] = "BEDROOM", + [self.BORDER] = "BORDER", + [self.BOXROOM] = "BOXROOM", + [self.BREAKFAST_ROOM] = "BREAKFAST_ROOM", + [self.CARPORT] = "CARPORT", + [self.CELLAR] = "CELLAR", + [self.CLOAKROOM] = "CLOAKROOM", + [self.CLOSET] = "CLOSET", + [self.CONSERVATORY] = "CONSERVATORY", + [self.CORRIDOR] = "CORRIDOR", + [self.CRAFT_ROOM] = "CRAFT_ROOM", + [self.CUPBOARD] = "CUPBOARD", + [self.DECK] = "DECK", + [self.DEN] = "DEN", + [self.DINING] = "DINING", + [self.DRAWING_ROOM] = "DRAWING_ROOM", + [self.DRESSING_ROOM] = "DRESSING_ROOM", + [self.DRIVEWAY] = "DRIVEWAY", + [self.ELEVATOR] = "ELEVATOR", + [self.ENSUITE] = "ENSUITE", + [self.ENTRANCE] = "ENTRANCE", + [self.ENTRYWAY] = "ENTRYWAY", + [self.FAMILY_ROOM] = "FAMILY_ROOM", + [self.FOYER] = "FOYER", + [self.FRONT_DOOR] = "FRONT_DOOR", + [self.FRONT_YARD] = "FRONT_YARD", + [self.GAME_ROOM] = "GAME_ROOM", + [self.GARAGE] = "GARAGE", + [self.GARAGE_DOOR] = "GARAGE_DOOR", + [self.GARDEN] = "GARDEN", + [self.GARDEN_DOOR] = "GARDEN_DOOR", + [self.GUEST_BATHROOM] = "GUEST_BATHROOM", + [self.GUEST_BEDROOM] = "GUEST_BEDROOM", + [self.GUEST_RESTROOM] = "GUEST_RESTROOM", + [self.GUEST_ROOM] = "GUEST_ROOM", + [self.GYM] = "GYM", + [self.HALLWAY] = "HALLWAY", + [self.HEARTH_ROOM] = "HEARTH_ROOM", + [self.KIDS_ROOM] = "KIDS_ROOM", + [self.KIDS_BEDROOM] = "KIDS_BEDROOM", + [self.KITCHEN] = "KITCHEN", + [self.LARDER] = "LARDER", + [self.LAUNDRY_ROOM] = "LAUNDRY_ROOM", + [self.LAWN] = "LAWN", + [self.LIBRARY] = "LIBRARY", + [self.LIVING_ROOM] = "LIVING_ROOM", + [self.LOUNGE] = "LOUNGE", + [self.MEDIA_TV_ROOM] = "MEDIA_TV_ROOM", + [self.MUD_ROOM] = "MUD_ROOM", + [self.MUSIC_ROOM] = "MUSIC_ROOM", + [self.NURSERY] = "NURSERY", + [self.OFFICE] = "OFFICE", + [self.OUTDOOR_KITCHEN] = "OUTDOOR_KITCHEN", + [self.OUTSIDE] = "OUTSIDE", + [self.PANTRY] = "PANTRY", + [self.PARKING_LOT] = "PARKING_LOT", + [self.PARLOR] = "PARLOR", + [self.PATIO] = "PATIO", + [self.PLAY_ROOM] = "PLAY_ROOM", + [self.POOL_ROOM] = "POOL_ROOM", + [self.PORCH] = "PORCH", + [self.PRIMARY_BATHROOM] = "PRIMARY_BATHROOM", + [self.PRIMARY_BEDROOM] = "PRIMARY_BEDROOM", + [self.RAMP] = "RAMP", + [self.RECEPTION_ROOM] = "RECEPTION_ROOM", + [self.RECREATION_ROOM] = "RECREATION_ROOM", + [self.RESTROOM] = "RESTROOM", + [self.ROOF] = "ROOF", + [self.SAUNA] = "SAUNA", + [self.SCULLERY] = "SCULLERY", + [self.SEWING_ROOM] = "SEWING_ROOM", + [self.SHED] = "SHED", + [self.SIDE_DOOR] = "SIDE_DOOR", + [self.SIDE_YARD] = "SIDE_YARD", + [self.SITTING_ROOM] = "SITTING_ROOM", + [self.SNUG] = "SNUG", + [self.SPA] = "SPA", + [self.STAIRCASE] = "STAIRCASE", + [self.STEAM_ROOM] = "STEAM_ROOM", + [self.STORAGE_ROOM] = "STORAGE_ROOM", + [self.STUDIO] = "STUDIO", + [self.STUDY] = "STUDY", + [self.SUN_ROOM] = "SUN_ROOM", + [self.SWIMMING_POOL] = "SWIMMING_POOL", + [self.TERRACE] = "TERRACE", + [self.UTILITY_ROOM] = "UTILITY_ROOM", + [self.WARD] = "WARD", + [self.WORKSHOP] = "WORKSHOP", + } + 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.AISLE = 0x00 +new_mt.__index.ATTIC = 0x01 +new_mt.__index.BACK_DOOR = 0x02 +new_mt.__index.BACK_YARD = 0x03 +new_mt.__index.BALCONY = 0x04 +new_mt.__index.BALLROOM = 0x05 +new_mt.__index.BATHROOM = 0x06 +new_mt.__index.BEDROOM = 0x07 +new_mt.__index.BORDER = 0x08 +new_mt.__index.BOXROOM = 0x09 +new_mt.__index.BREAKFAST_ROOM = 0x0A +new_mt.__index.CARPORT = 0x0B +new_mt.__index.CELLAR = 0x0C +new_mt.__index.CLOAKROOM = 0x0D +new_mt.__index.CLOSET = 0x0E +new_mt.__index.CONSERVATORY = 0x0F +new_mt.__index.CORRIDOR = 0x10 +new_mt.__index.CRAFT_ROOM = 0x11 +new_mt.__index.CUPBOARD = 0x12 +new_mt.__index.DECK = 0x13 +new_mt.__index.DEN = 0x14 +new_mt.__index.DINING = 0x15 +new_mt.__index.DRAWING_ROOM = 0x16 +new_mt.__index.DRESSING_ROOM = 0x17 +new_mt.__index.DRIVEWAY = 0x18 +new_mt.__index.ELEVATOR = 0x19 +new_mt.__index.ENSUITE = 0x1A +new_mt.__index.ENTRANCE = 0x1B +new_mt.__index.ENTRYWAY = 0x1C +new_mt.__index.FAMILY_ROOM = 0x1D +new_mt.__index.FOYER = 0x1E +new_mt.__index.FRONT_DOOR = 0x1F +new_mt.__index.FRONT_YARD = 0x20 +new_mt.__index.GAME_ROOM = 0x21 +new_mt.__index.GARAGE = 0x22 +new_mt.__index.GARAGE_DOOR = 0x23 +new_mt.__index.GARDEN = 0x24 +new_mt.__index.GARDEN_DOOR = 0x25 +new_mt.__index.GUEST_BATHROOM = 0x26 +new_mt.__index.GUEST_BEDROOM = 0x27 +new_mt.__index.GUEST_RESTROOM = 0x28 +new_mt.__index.GUEST_ROOM = 0x29 +new_mt.__index.GYM = 0x2A +new_mt.__index.HALLWAY = 0x2B +new_mt.__index.HEARTH_ROOM = 0x2C +new_mt.__index.KIDS_ROOM = 0x2D +new_mt.__index.KIDS_BEDROOM = 0x2E +new_mt.__index.KITCHEN = 0x2F +new_mt.__index.LARDER = 0x30 +new_mt.__index.LAUNDRY_ROOM = 0x31 +new_mt.__index.LAWN = 0x32 +new_mt.__index.LIBRARY = 0x33 +new_mt.__index.LIVING_ROOM = 0x34 +new_mt.__index.LOUNGE = 0x35 +new_mt.__index.MEDIA_TV_ROOM = 0x36 +new_mt.__index.MUD_ROOM = 0x37 +new_mt.__index.MUSIC_ROOM = 0x38 +new_mt.__index.NURSERY = 0x39 +new_mt.__index.OFFICE = 0x3A +new_mt.__index.OUTDOOR_KITCHEN = 0x3B +new_mt.__index.OUTSIDE = 0x3C +new_mt.__index.PANTRY = 0x3D +new_mt.__index.PARKING_LOT = 0x3E +new_mt.__index.PARLOR = 0x3F +new_mt.__index.PATIO = 0x40 +new_mt.__index.PLAY_ROOM = 0x41 +new_mt.__index.POOL_ROOM = 0x42 +new_mt.__index.PORCH = 0x43 +new_mt.__index.PRIMARY_BATHROOM = 0x44 +new_mt.__index.PRIMARY_BEDROOM = 0x45 +new_mt.__index.RAMP = 0x46 +new_mt.__index.RECEPTION_ROOM = 0x47 +new_mt.__index.RECREATION_ROOM = 0x48 +new_mt.__index.RESTROOM = 0x49 +new_mt.__index.ROOF = 0x4A +new_mt.__index.SAUNA = 0x4B +new_mt.__index.SCULLERY = 0x4C +new_mt.__index.SEWING_ROOM = 0x4D +new_mt.__index.SHED = 0x4E +new_mt.__index.SIDE_DOOR = 0x4F +new_mt.__index.SIDE_YARD = 0x50 +new_mt.__index.SITTING_ROOM = 0x51 +new_mt.__index.SNUG = 0x52 +new_mt.__index.SPA = 0x53 +new_mt.__index.STAIRCASE = 0x54 +new_mt.__index.STEAM_ROOM = 0x55 +new_mt.__index.STORAGE_ROOM = 0x56 +new_mt.__index.STUDIO = 0x57 +new_mt.__index.STUDY = 0x58 +new_mt.__index.SUN_ROOM = 0x59 +new_mt.__index.SWIMMING_POOL = 0x5A +new_mt.__index.TERRACE = 0x5B +new_mt.__index.UTILITY_ROOM = 0x5C +new_mt.__index.WARD = 0x5D +new_mt.__index.WORKSHOP = 0x5E + +AreaTypeTag.AISLE = 0x00 +AreaTypeTag.ATTIC = 0x01 +AreaTypeTag.BACK_DOOR = 0x02 +AreaTypeTag.BACK_YARD = 0x03 +AreaTypeTag.BALCONY = 0x04 +AreaTypeTag.BALLROOM = 0x05 +AreaTypeTag.BATHROOM = 0x06 +AreaTypeTag.BEDROOM = 0x07 +AreaTypeTag.BORDER = 0x08 +AreaTypeTag.BOXROOM = 0x09 +AreaTypeTag.BREAKFAST_ROOM = 0x0A +AreaTypeTag.CARPORT = 0x0B +AreaTypeTag.CELLAR = 0x0C +AreaTypeTag.CLOAKROOM = 0x0D +AreaTypeTag.CLOSET = 0x0E +AreaTypeTag.CONSERVATORY = 0x0F +AreaTypeTag.CORRIDOR = 0x10 +AreaTypeTag.CRAFT_ROOM = 0x11 +AreaTypeTag.CUPBOARD = 0x12 +AreaTypeTag.DECK = 0x13 +AreaTypeTag.DEN = 0x14 +AreaTypeTag.DINING = 0x15 +AreaTypeTag.DRAWING_ROOM = 0x16 +AreaTypeTag.DRESSING_ROOM = 0x17 +AreaTypeTag.DRIVEWAY = 0x18 +AreaTypeTag.ELEVATOR = 0x19 +AreaTypeTag.ENSUITE = 0x1A +AreaTypeTag.ENTRANCE = 0x1B +AreaTypeTag.ENTRYWAY = 0x1C +AreaTypeTag.FAMILY_ROOM = 0x1D +AreaTypeTag.FOYER = 0x1E +AreaTypeTag.FRONT_DOOR = 0x1F +AreaTypeTag.FRONT_YARD = 0x20 +AreaTypeTag.GAME_ROOM = 0x21 +AreaTypeTag.GARAGE = 0x22 +AreaTypeTag.GARAGE_DOOR = 0x23 +AreaTypeTag.GARDEN = 0x24 +AreaTypeTag.GARDEN_DOOR = 0x25 +AreaTypeTag.GUEST_BATHROOM = 0x26 +AreaTypeTag.GUEST_BEDROOM = 0x27 +AreaTypeTag.GUEST_RESTROOM = 0x28 +AreaTypeTag.GUEST_ROOM = 0x29 +AreaTypeTag.GYM = 0x2A +AreaTypeTag.HALLWAY = 0x2B +AreaTypeTag.HEARTH_ROOM = 0x2C +AreaTypeTag.KIDS_ROOM = 0x2D +AreaTypeTag.KIDS_BEDROOM = 0x2E +AreaTypeTag.KITCHEN = 0x2F +AreaTypeTag.LARDER = 0x30 +AreaTypeTag.LAUNDRY_ROOM = 0x31 +AreaTypeTag.LAWN = 0x32 +AreaTypeTag.LIBRARY = 0x33 +AreaTypeTag.LIVING_ROOM = 0x34 +AreaTypeTag.LOUNGE = 0x35 +AreaTypeTag.MEDIA_TV_ROOM = 0x36 +AreaTypeTag.MUD_ROOM = 0x37 +AreaTypeTag.MUSIC_ROOM = 0x38 +AreaTypeTag.NURSERY = 0x39 +AreaTypeTag.OFFICE = 0x3A +AreaTypeTag.OUTDOOR_KITCHEN = 0x3B +AreaTypeTag.OUTSIDE = 0x3C +AreaTypeTag.PANTRY = 0x3D +AreaTypeTag.PARKING_LOT = 0x3E +AreaTypeTag.PARLOR = 0x3F +AreaTypeTag.PATIO = 0x40 +AreaTypeTag.PLAY_ROOM = 0x41 +AreaTypeTag.POOL_ROOM = 0x42 +AreaTypeTag.PORCH = 0x43 +AreaTypeTag.PRIMARY_BATHROOM = 0x44 +AreaTypeTag.PRIMARY_BEDROOM = 0x45 +AreaTypeTag.RAMP = 0x46 +AreaTypeTag.RECEPTION_ROOM = 0x47 +AreaTypeTag.RECREATION_ROOM = 0x48 +AreaTypeTag.RESTROOM = 0x49 +AreaTypeTag.ROOF = 0x4A +AreaTypeTag.SAUNA = 0x4B +AreaTypeTag.SCULLERY = 0x4C +AreaTypeTag.SEWING_ROOM = 0x4D +AreaTypeTag.SHED = 0x4E +AreaTypeTag.SIDE_DOOR = 0x4F +AreaTypeTag.SIDE_YARD = 0x50 +AreaTypeTag.SITTING_ROOM = 0x51 +AreaTypeTag.SNUG = 0x52 +AreaTypeTag.SPA = 0x53 +AreaTypeTag.STAIRCASE = 0x54 +AreaTypeTag.STEAM_ROOM = 0x55 +AreaTypeTag.STORAGE_ROOM = 0x56 +AreaTypeTag.STUDIO = 0x57 +AreaTypeTag.STUDY = 0x58 +AreaTypeTag.SUN_ROOM = 0x59 +AreaTypeTag.SWIMMING_POOL = 0x5A +AreaTypeTag.TERRACE = 0x5B +AreaTypeTag.UTILITY_ROOM = 0x5C +AreaTypeTag.WARD = 0x5D +AreaTypeTag.WORKSHOP = 0x5E + +AreaTypeTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(AreaTypeTag, new_mt) + +return AreaTypeTag + diff --git a/drivers/SmartThings/matter-rvc/src/Global/types/LandmarkTag.lua b/drivers/SmartThings/matter-rvc/src/Global/types/LandmarkTag.lua new file mode 100644 index 0000000000..1c70376a78 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/types/LandmarkTag.lua @@ -0,0 +1,247 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class Global.types.LandmarkTag: st.matter.data_types.Uint8 +--- @alias LandmarkTag +--- +--- @field public byte_length number 1 +--- @field public AIR_CONDITIONER number 0 +--- @field public AIR_PURIFIER number 1 +--- @field public BACK_DOOR number 2 +--- @field public BAR_STOOL number 3 +--- @field public BATH_MAT number 4 +--- @field public BATHTUB number 5 +--- @field public BED number 6 +--- @field public BOOKSHELF number 7 +--- @field public CHAIR number 8 +--- @field public CHRISTMAS_TREE number 9 +--- @field public COAT_RACK number 10 +--- @field public COFFEE_TABLE number 11 +--- @field public COOKING_RANGE number 12 +--- @field public COUCH number 13 +--- @field public COUNTERTOP number 14 +--- @field public CRADLE number 15 +--- @field public CRIB number 16 +--- @field public DESK number 17 +--- @field public DINING_TABLE number 18 +--- @field public DISHWASHER number 19 +--- @field public DOOR number 20 +--- @field public DRESSER number 21 +--- @field public LAUNDRY_DRYER number 22 +--- @field public FAN number 23 +--- @field public FIREPLACE number 24 +--- @field public FREEZER number 25 +--- @field public FRONT_DOOR number 26 +--- @field public HIGH_CHAIR number 27 +--- @field public KITCHEN_ISLAND number 28 +--- @field public LAMP number 29 +--- @field public LITTER_BOX number 30 +--- @field public MIRROR number 31 +--- @field public NIGHTSTAND number 32 +--- @field public OVEN number 33 +--- @field public PET_BED number 34 +--- @field public PET_BOWL number 35 +--- @field public PET_CRATE number 36 +--- @field public REFRIGERATOR number 37 +--- @field public SCRATCHING_POST number 38 +--- @field public SHOE_RACK number 39 +--- @field public SHOWER number 40 +--- @field public SIDE_DOOR number 41 +--- @field public SINK number 42 +--- @field public SOFA number 43 +--- @field public STOVE number 44 +--- @field public TABLE number 45 +--- @field public TOILET number 46 +--- @field public TRASH_CAN number 47 +--- @field public LAUNDRY_WASHER number 48 +--- @field public WINDOW number 49 +--- @field public WINE_COOLER number 50 + +local LandmarkTag = {} +local new_mt = UintABC.new_mt({NAME = "LandmarkTag", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.AIR_CONDITIONER] = "AIR_CONDITIONER", + [self.AIR_PURIFIER] = "AIR_PURIFIER", + [self.BACK_DOOR] = "BACK_DOOR", + [self.BAR_STOOL] = "BAR_STOOL", + [self.BATH_MAT] = "BATH_MAT", + [self.BATHTUB] = "BATHTUB", + [self.BED] = "BED", + [self.BOOKSHELF] = "BOOKSHELF", + [self.CHAIR] = "CHAIR", + [self.CHRISTMAS_TREE] = "CHRISTMAS_TREE", + [self.COAT_RACK] = "COAT_RACK", + [self.COFFEE_TABLE] = "COFFEE_TABLE", + [self.COOKING_RANGE] = "COOKING_RANGE", + [self.COUCH] = "COUCH", + [self.COUNTERTOP] = "COUNTERTOP", + [self.CRADLE] = "CRADLE", + [self.CRIB] = "CRIB", + [self.DESK] = "DESK", + [self.DINING_TABLE] = "DINING_TABLE", + [self.DISHWASHER] = "DISHWASHER", + [self.DOOR] = "DOOR", + [self.DRESSER] = "DRESSER", + [self.LAUNDRY_DRYER] = "LAUNDRY_DRYER", + [self.FAN] = "FAN", + [self.FIREPLACE] = "FIREPLACE", + [self.FREEZER] = "FREEZER", + [self.FRONT_DOOR] = "FRONT_DOOR", + [self.HIGH_CHAIR] = "HIGH_CHAIR", + [self.KITCHEN_ISLAND] = "KITCHEN_ISLAND", + [self.LAMP] = "LAMP", + [self.LITTER_BOX] = "LITTER_BOX", + [self.MIRROR] = "MIRROR", + [self.NIGHTSTAND] = "NIGHTSTAND", + [self.OVEN] = "OVEN", + [self.PET_BED] = "PET_BED", + [self.PET_BOWL] = "PET_BOWL", + [self.PET_CRATE] = "PET_CRATE", + [self.REFRIGERATOR] = "REFRIGERATOR", + [self.SCRATCHING_POST] = "SCRATCHING_POST", + [self.SHOE_RACK] = "SHOE_RACK", + [self.SHOWER] = "SHOWER", + [self.SIDE_DOOR] = "SIDE_DOOR", + [self.SINK] = "SINK", + [self.SOFA] = "SOFA", + [self.STOVE] = "STOVE", + [self.TABLE] = "TABLE", + [self.TOILET] = "TOILET", + [self.TRASH_CAN] = "TRASH_CAN", + [self.LAUNDRY_WASHER] = "LAUNDRY_WASHER", + [self.WINDOW] = "WINDOW", + [self.WINE_COOLER] = "WINE_COOLER", + } + 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.AIR_CONDITIONER = 0x00 +new_mt.__index.AIR_PURIFIER = 0x01 +new_mt.__index.BACK_DOOR = 0x02 +new_mt.__index.BAR_STOOL = 0x03 +new_mt.__index.BATH_MAT = 0x04 +new_mt.__index.BATHTUB = 0x05 +new_mt.__index.BED = 0x06 +new_mt.__index.BOOKSHELF = 0x07 +new_mt.__index.CHAIR = 0x08 +new_mt.__index.CHRISTMAS_TREE = 0x09 +new_mt.__index.COAT_RACK = 0x0A +new_mt.__index.COFFEE_TABLE = 0x0B +new_mt.__index.COOKING_RANGE = 0x0C +new_mt.__index.COUCH = 0x0D +new_mt.__index.COUNTERTOP = 0x0E +new_mt.__index.CRADLE = 0x0F +new_mt.__index.CRIB = 0x10 +new_mt.__index.DESK = 0x11 +new_mt.__index.DINING_TABLE = 0x12 +new_mt.__index.DISHWASHER = 0x13 +new_mt.__index.DOOR = 0x14 +new_mt.__index.DRESSER = 0x15 +new_mt.__index.LAUNDRY_DRYER = 0x16 +new_mt.__index.FAN = 0x17 +new_mt.__index.FIREPLACE = 0x18 +new_mt.__index.FREEZER = 0x19 +new_mt.__index.FRONT_DOOR = 0x1A +new_mt.__index.HIGH_CHAIR = 0x1B +new_mt.__index.KITCHEN_ISLAND = 0x1C +new_mt.__index.LAMP = 0x1D +new_mt.__index.LITTER_BOX = 0x1E +new_mt.__index.MIRROR = 0x1F +new_mt.__index.NIGHTSTAND = 0x20 +new_mt.__index.OVEN = 0x21 +new_mt.__index.PET_BED = 0x22 +new_mt.__index.PET_BOWL = 0x23 +new_mt.__index.PET_CRATE = 0x24 +new_mt.__index.REFRIGERATOR = 0x25 +new_mt.__index.SCRATCHING_POST = 0x26 +new_mt.__index.SHOE_RACK = 0x27 +new_mt.__index.SHOWER = 0x28 +new_mt.__index.SIDE_DOOR = 0x29 +new_mt.__index.SINK = 0x2A +new_mt.__index.SOFA = 0x2B +new_mt.__index.STOVE = 0x2C +new_mt.__index.TABLE = 0x2D +new_mt.__index.TOILET = 0x2E +new_mt.__index.TRASH_CAN = 0x2F +new_mt.__index.LAUNDRY_WASHER = 0x30 +new_mt.__index.WINDOW = 0x31 +new_mt.__index.WINE_COOLER = 0x32 + +LandmarkTag.AIR_CONDITIONER = 0x00 +LandmarkTag.AIR_PURIFIER = 0x01 +LandmarkTag.BACK_DOOR = 0x02 +LandmarkTag.BAR_STOOL = 0x03 +LandmarkTag.BATH_MAT = 0x04 +LandmarkTag.BATHTUB = 0x05 +LandmarkTag.BED = 0x06 +LandmarkTag.BOOKSHELF = 0x07 +LandmarkTag.CHAIR = 0x08 +LandmarkTag.CHRISTMAS_TREE = 0x09 +LandmarkTag.COAT_RACK = 0x0A +LandmarkTag.COFFEE_TABLE = 0x0B +LandmarkTag.COOKING_RANGE = 0x0C +LandmarkTag.COUCH = 0x0D +LandmarkTag.COUNTERTOP = 0x0E +LandmarkTag.CRADLE = 0x0F +LandmarkTag.CRIB = 0x10 +LandmarkTag.DESK = 0x11 +LandmarkTag.DINING_TABLE = 0x12 +LandmarkTag.DISHWASHER = 0x13 +LandmarkTag.DOOR = 0x14 +LandmarkTag.DRESSER = 0x15 +LandmarkTag.LAUNDRY_DRYER = 0x16 +LandmarkTag.FAN = 0x17 +LandmarkTag.FIREPLACE = 0x18 +LandmarkTag.FREEZER = 0x19 +LandmarkTag.FRONT_DOOR = 0x1A +LandmarkTag.HIGH_CHAIR = 0x1B +LandmarkTag.KITCHEN_ISLAND = 0x1C +LandmarkTag.LAMP = 0x1D +LandmarkTag.LITTER_BOX = 0x1E +LandmarkTag.MIRROR = 0x1F +LandmarkTag.NIGHTSTAND = 0x20 +LandmarkTag.OVEN = 0x21 +LandmarkTag.PET_BED = 0x22 +LandmarkTag.PET_BOWL = 0x23 +LandmarkTag.PET_CRATE = 0x24 +LandmarkTag.REFRIGERATOR = 0x25 +LandmarkTag.SCRATCHING_POST = 0x26 +LandmarkTag.SHOE_RACK = 0x27 +LandmarkTag.SHOWER = 0x28 +LandmarkTag.SIDE_DOOR = 0x29 +LandmarkTag.SINK = 0x2A +LandmarkTag.SOFA = 0x2B +LandmarkTag.STOVE = 0x2C +LandmarkTag.TABLE = 0x2D +LandmarkTag.TOILET = 0x2E +LandmarkTag.TRASH_CAN = 0x2F +LandmarkTag.LAUNDRY_WASHER = 0x30 +LandmarkTag.WINDOW = 0x31 +LandmarkTag.WINE_COOLER = 0x32 + +LandmarkTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(LandmarkTag, new_mt) + +return LandmarkTag + diff --git a/drivers/SmartThings/matter-rvc/src/Global/types/LocationDescriptorStruct.lua b/drivers/SmartThings/matter-rvc/src/Global/types/LocationDescriptorStruct.lua new file mode 100644 index 0000000000..facd03def3 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/types/LocationDescriptorStruct.lua @@ -0,0 +1,106 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +--- @alias LocationDescriptorStruct +--- @class Global.types.LocationDescriptorStruct: st.matter.data_types.Structure +--- +--- @field public location_name st.matter.data_types.UTF8String1 +--- @field public floor_number st.matter.data_types.Int16 +--- @field public area_type Global.types.AreaTypeTag +local LocationDescriptorStruct = {} +local new_mt = StructureABC.new_mt({NAME = "LocationDescriptorStruct", ID = data_types.name_to_id_map["Structure"]}) + +LocationDescriptorStruct.field_defs = { + { + name = "location_name", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, + { + name = "floor_number", + field_id = 1, + is_nullable = true, + is_optional = false, + data_type = require "st.matter.data_types.Int16", + }, + { + name = "area_type", + field_id = 2, + is_nullable = true, + is_optional = false, + data_type = require "Global.types.AreaTypeTag", + }, +} + +LocationDescriptorStruct.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) + else + 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 + +LocationDescriptorStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = LocationDescriptorStruct.init +new_mt.__index.serialize = LocationDescriptorStruct.serialize + +LocationDescriptorStruct.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) 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(LocationDescriptorStruct, new_mt) + +return LocationDescriptorStruct + diff --git a/drivers/SmartThings/matter-rvc/src/Global/types/RelativePositionTag.lua b/drivers/SmartThings/matter-rvc/src/Global/types/RelativePositionTag.lua new file mode 100644 index 0000000000..d351e592a9 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/types/RelativePositionTag.lua @@ -0,0 +1,71 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class Global.types.RelativePositionTag: st.matter.data_types.Uint8 +--- @alias RelativePositionTag +--- +--- @field public byte_length number 1 +--- @field public UNDER number 0 +--- @field public NEXT_TO number 1 +--- @field public AROUND number 2 +--- @field public ON number 3 +--- @field public ABOVE number 4 +--- @field public FRONT_OF number 5 +--- @field public BEHIND number 6 + +local RelativePositionTag = {} +local new_mt = UintABC.new_mt({NAME = "RelativePositionTag", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.UNDER] = "UNDER", + [self.NEXT_TO] = "NEXT_TO", + [self.AROUND] = "AROUND", + [self.ON] = "ON", + [self.ABOVE] = "ABOVE", + [self.FRONT_OF] = "FRONT_OF", + [self.BEHIND] = "BEHIND", + } + 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.UNDER = 0x00 +new_mt.__index.NEXT_TO = 0x01 +new_mt.__index.AROUND = 0x02 +new_mt.__index.ON = 0x03 +new_mt.__index.ABOVE = 0x04 +new_mt.__index.FRONT_OF = 0x05 +new_mt.__index.BEHIND = 0x06 + +RelativePositionTag.UNDER = 0x00 +RelativePositionTag.NEXT_TO = 0x01 +RelativePositionTag.AROUND = 0x02 +RelativePositionTag.ON = 0x03 +RelativePositionTag.ABOVE = 0x04 +RelativePositionTag.FRONT_OF = 0x05 +RelativePositionTag.BEHIND = 0x06 + +RelativePositionTag.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(RelativePositionTag, new_mt) + +return RelativePositionTag + diff --git a/drivers/SmartThings/matter-rvc/src/Global/types/init.lua b/drivers/SmartThings/matter-rvc/src/Global/types/init.lua new file mode 100644 index 0000000000..55f3c97bd7 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/Global/types/init.lua @@ -0,0 +1,36 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +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("Global.types." .. key) + end + return types_mt.__types_cache[key] +end + +--- @class GlobalTypes +--- +--- @field public RelativePositionTag Global.types.RelativePositionTag +--- @field public LandmarkTag Global.types.LandmarkTag +--- @field public AreaTypeTag Global.types.AreaTypeTag + +local GlobalTypes = {} + +setmetatable(GlobalTypes, types_mt) + +return GlobalTypes diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/SelectAreasResponse.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/SelectAreasResponse.lua new file mode 100644 index 0000000000..b27711b98b --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/SelectAreasResponse.lua @@ -0,0 +1,150 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +----------------------------------------------------------- +-- ServiceArea command SelectAreasResponse +----------------------------------------------------------- + +--- @class st.matter.clusters.ServiceArea.SelectAreasResponse +--- @alias SelectAreasResponse +--- +--- @field public ID number 0x0001 the ID of this command +--- @field public NAME string "SelectAreasResponse" the name of this command +--- @field public status ServiceArea.types.SelectAreasStatus +--- @field public status_text st.matter.data_types.UTF8String1 +local SelectAreasResponse = {} + +SelectAreasResponse.NAME = "SelectAreasResponse" +SelectAreasResponse.ID = 0x0001 +SelectAreasResponse.field_defs = { + { + name = "status", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "ServiceArea.types.SelectAreasStatus", + }, + { + name = "status_text", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.UTF8String1", + }, +} + +--- Add field names to each command field +--- +--- @param base_type_obj st.matter.data_types.Structure +function SelectAreasResponse:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +--- Builds an SelectAreasResponse test command reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message to +--- @param endpoint_id number|nil +--- @param status ServiceArea.types.SelectAreasStatus +--- @param status_text st.matter.data_types.UTF8String1 +--- @return st.matter.st.matter.interaction_model.InteractionResponse of type COMMAND_RESPONSE +function SelectAreasResponse:build_test_command_response(device, endpoint_id, status, status_text, interaction_status) + local function init(self, device, endpoint_id, status, status_text) + local out = {} + local args = {status, status_text} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = SelectAreasResponse, + __tostring = SelectAreasResponse.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + false, + true + ) + end + local self_request = init(self, device, endpoint_id, status, status_text) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + self_request.info_blocks[1].tlv, + interaction_status + ) +end + +--- Initialize the SelectAreasResponse command +--- +--- @return nil +function SelectAreasResponse:init() + return nil +end + +function SelectAreasResponse:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function SelectAreasResponse:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SelectAreasResponse, {__call = SelectAreasResponse.init}) + +return SelectAreasResponse diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/init.lua new file mode 100644 index 0000000000..85af7a6f7c --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/client/commands/init.lua @@ -0,0 +1,42 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ServiceArea.client.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +--- @class ServiceAreaClientCommands +--- +--- @field public SelectAreasResponse ServiceArea.SelectAreasResponse +local ServiceAreaClientCommands = {} + +function ServiceAreaClientCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ServiceAreaClientCommands, command_mt) + +return ServiceAreaClientCommands + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/init.lua new file mode 100644 index 0000000000..f06dbdd7cb --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/init.lua @@ -0,0 +1,183 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local ServiceAreaServerAttributes = require "ServiceArea.server.attributes" +local ServiceAreaServerCommands = require "ServiceArea.server.commands" +local ServiceAreaClientCommands = require "ServiceArea.client.commands" +local ServiceAreaTypes = require "ServiceArea.types" + +--- @class ServiceArea +--- @alias ServiceArea +--- +--- @field public ID number 0x0150 the ID of this cluster +--- @field public NAME string "ServiceArea" the name of this cluster +--- @field public attributes ServiceAreaServerAttributes | ServiceAreaClientAttributes +--- @field public commands ServiceAreaServerCommands | ServiceAreaClientCommands +--- @field public types ServiceAreaTypes + +local ServiceArea = {} + +ServiceArea.ID = 0x0150 +ServiceArea.NAME = "ServiceArea" +ServiceArea.server = {} +ServiceArea.client = {} +ServiceArea.server.attributes = ServiceAreaServerAttributes:set_parent_cluster(ServiceArea) +ServiceArea.server.commands = ServiceAreaServerCommands:set_parent_cluster(ServiceArea) +ServiceArea.client.commands = ServiceAreaClientCommands:set_parent_cluster(ServiceArea) +ServiceArea.types = ServiceAreaTypes + +--- Find an attribute by id +--- +--- @param attr_id number +function ServiceArea:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "SupportedAreas", + [0x0001] = "SupportedMaps", + [0x0002] = "SelectedAreas", + [0x0003] = "CurrentArea", + [0x0004] = "EstimatedEndTime", + [0x0005] = "Progress", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +--- Find a server command by id +--- +--- @param command_id number +function ServiceArea:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "SelectAreas", + [0x0002] = "SkipArea", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +--- Find a client command by id +--- +--- @param command_id number +function ServiceArea:get_client_command_by_id(command_id) + local client_id_map = { + [0x0001] = "SelectAreasResponse", + [0x0003] = "SkipAreaResponse", + } + if client_id_map[command_id] ~= nil then + return self.client.commands[client_id_map[command_id]] + end + return nil +end + +-- Attribute Mapping +ServiceArea.attribute_direction_map = { + ["SupportedAreas"] = "server", + ["SupportedMaps"] = "server", + ["SelectedAreas"] = "server", + ["CurrentArea"] = "server", + ["EstimatedEndTime"] = "server", + ["Progress"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "aliases.ServiceArea.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + ServiceArea.attribute_direction_map[alias] = "server" + end + end +end + +-- Command Mapping +ServiceArea.command_direction_map = { + ["SelectAreas"] = "server", + ["SkipArea"] = "server", + ["SelectAreasResponse"] = "client", + ["SkipAreaResponse"] = "client", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ServiceArea.server.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ServiceArea.command_direction_map[alias] = "server" + end + end +end + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ServiceArea.client.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ServiceArea.command_direction_map[alias] = "client" + end + end +end + +ServiceArea.FeatureMap = ServiceArea.types.Feature + +function ServiceArea.are_features_supported(feature, feature_map) + if (ServiceArea.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +-- Cluster Completion +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ServiceArea.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ServiceArea.NAME)) + end + return ServiceArea[direction].attributes[key] +end +ServiceArea.attributes = {} +setmetatable(ServiceArea.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ServiceArea.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ServiceArea.NAME)) + end + return ServiceArea[direction].commands[key] +end +ServiceArea.commands = {} +setmetatable(ServiceArea.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ServiceArea.server.events[key] +end +ServiceArea.events = {} +setmetatable(ServiceArea.events, event_helper_mt) + +setmetatable(ServiceArea, {__index = cluster_base}) + +return ServiceArea + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SelectedAreas.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SelectedAreas.lua new file mode 100644 index 0000000000..01ff53c95c --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SelectedAreas.lua @@ -0,0 +1,126 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.ServiceArea.SelectedAreas +--- @alias SelectedAreas +--- +--- @field public ID number 0x0002 the ID of this attribute +--- @field public NAME string "SelectedAreas" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local SelectedAreas = { + ID = 0x0002, + NAME = "SelectedAreas", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function SelectedAreas: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, SelectedAreas.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the SelectedAreas(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function SelectedAreas:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function SelectedAreas:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: SelectedAreas => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function SelectedAreas:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SelectedAreas:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an SelectedAreas test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function SelectedAreas: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 SelectedAreas:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(SelectedAreas, {__call = SelectedAreas.new_value, __index = SelectedAreas.base_type}) +return SelectedAreas + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SupportedAreas.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SupportedAreas.lua new file mode 100644 index 0000000000..205f907b64 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/SupportedAreas.lua @@ -0,0 +1,126 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +--- @class st.matter.clusters.ServiceArea.SupportedAreas +--- @alias SupportedAreas +--- +--- @field public ID number 0x0000 the ID of this attribute +--- @field public NAME string "SupportedAreas" the name of this attribute +--- @field public data_type st.matter.data_types.Array the data type of this attribute + +local SupportedAreas = { + ID = 0x0000, + NAME = "SupportedAreas", + base_type = require "st.matter.data_types.Array", + element_type = require "ServiceArea.types.AreaStruct", +} + +--- Add additional functionality to the base type object +--- +--- @param base_type_obj st.matter.data_types.Array the base data type object to add functionality to +function SupportedAreas: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, SupportedAreas.element_type) + end +end + +--- Create a Array object of this attribute with any additional features provided for the attribute +--- This is also usable with the SupportedAreas(...) syntax +--- +--- @vararg vararg the values needed to construct a Array +--- @return st.matter.data_types.Array +function SupportedAreas:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +--- Constructs an st.matter.interaction_model.InteractionRequest to read +--- this attribute from a device +--- @param device st.matter.Device +--- @param endpoint_id number|nil +--- @return st.matter.interaction_model.InteractionRequest containing an Interaction Request +function SupportedAreas:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + + +--- Reporting policy: SupportedAreas => true => mandatory + +--- Sets up a Subscribe Interaction +--- +--- @param device any +--- @param endpoint_id number|nil +--- @return any +function SupportedAreas:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function SupportedAreas:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Builds an SupportedAreas test attribute reponse for the driver integration testing framework +--- +--- @param device st.matter.Device the device to build this message for +--- @param endpoint_id number|nil +--- @param value any +--- @param status string Interaction status associated with the path +--- @return st.matter.interaction_model.InteractionResponse of type REPORT_DATA +function SupportedAreas: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 SupportedAreas:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SupportedAreas, {__call = SupportedAreas.new_value, __index = SupportedAreas.base_type}) +return SupportedAreas + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/init.lua new file mode 100644 index 0000000000..6750f1fcdf --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/attributes/init.lua @@ -0,0 +1,44 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +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("ServiceArea.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 + +--- @class ServiceAreaServerAttributes +--- +--- @field public SupportedAreas ServiceArea.server.attributes.SupportedAreas +--- @field public SelectedAreas ServiceArea.server.attributes.SelectedAreas +local ServiceAreaServerAttributes = {} + +function ServiceAreaServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ServiceAreaServerAttributes, attr_mt) + +return ServiceAreaServerAttributes + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/SelectAreas.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/SelectAreas.lua new file mode 100644 index 0000000000..b62d393279 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/SelectAreas.lua @@ -0,0 +1,127 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +----------------------------------------------------------- +-- ServiceArea command SelectAreas +----------------------------------------------------------- + +--- @class st.matter.clusters.ServiceArea.SelectAreas +--- @alias SelectAreas +--- +--- @field public ID number 0x0000 the ID of this command +--- @field public NAME string "SelectAreas" the name of this command +--- @field public new_areas st.matter.data_types.Array +local SelectAreas = {} + +SelectAreas.NAME = "SelectAreas" +SelectAreas.ID = 0x0000 +SelectAreas.field_defs = { + { + name = "new_areas", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", + }, +} + +--- Refer to SelectAreasResponse:build_test_command_response for +--- building a test command reponse for the driver integration testing framework + +--- Initialize the SelectAreas command +--- +--- @param self SelectAreas the template class for this command +--- @param device st.matter.Device the device to build this message to +--- @param new_areas st.matter.data_types.Array + +--- @return st.matter.interaction_model.InteractionRequest of type INVOKE +function SelectAreas:init(device, endpoint_id, new_areas) + local out = {} + local args = {new_areas} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = SelectAreas, + __tostring = SelectAreas.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function SelectAreas:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +--- Add field names to each command field +--- +--- @param base_type_obj st.matter.data_types.Structure +function SelectAreas:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function SelectAreas:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(SelectAreas, {__call = SelectAreas.init}) + +return SelectAreas + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/init.lua new file mode 100644 index 0000000000..280462b1d0 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/commands/init.lua @@ -0,0 +1,42 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ServiceArea.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +--- @class ServiceAreaServerCommands +--- +--- @field public SelectAreas ServiceArea.SelectAreas +local ServiceAreaServerCommands = {} + +function ServiceAreaServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ServiceAreaServerCommands, command_mt) + +return ServiceAreaServerCommands + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/server/events/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/events/init.lua new file mode 100644 index 0000000000..d2d8ebaefe --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/server/events/init.lua @@ -0,0 +1,42 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local event_mt = {} +event_mt.__event_cache = {} +event_mt.__index = function(self, key) + if event_mt.__event_cache[key] == nil then + local req_loc = string.format("ServiceArea.server.events.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + event_mt.__event_cache[key] = raw_def + end + return event_mt.__event_cache[key] +end + +--- @class ServiceAreaEvents +--- +local ServiceAreaEvents = {} + +function ServiceAreaEvents:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ServiceAreaEvents, event_mt) + +return ServiceAreaEvents + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaInfoStruct.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaInfoStruct.lua new file mode 100644 index 0000000000..9e7932857d --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaInfoStruct.lua @@ -0,0 +1,98 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +--- @alias AreaInfoStruct +--- @class st.matter.clusters.ServiceArea.types.AreaInfoStruct: st.matter.data_types.Structure +--- +--- @field public location_info Global.types.LocationDescriptorStruct +--- @field public landmark_info ServiceArea.types.LandmarkInfoStruct +local AreaInfoStruct = {} +local new_mt = StructureABC.new_mt({NAME = "AreaInfoStruct", ID = data_types.name_to_id_map["Structure"]}) + +AreaInfoStruct.field_defs = { + { + name = "location_info", + field_id = 0, + is_nullable = true, + is_optional = false, + data_type = require "Global.types.LocationDescriptorStruct", + }, + { + name = "landmark_info", + field_id = 1, + is_nullable = true, + is_optional = false, + data_type = require "ServiceArea.types.LandmarkInfoStruct", + }, +} + +AreaInfoStruct.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) + else + 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 + +AreaInfoStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = AreaInfoStruct.init +new_mt.__index.serialize = AreaInfoStruct.serialize + +AreaInfoStruct.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) 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(AreaInfoStruct, new_mt) + +return AreaInfoStruct + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaStruct.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaStruct.lua new file mode 100644 index 0000000000..9277b81318 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/AreaStruct.lua @@ -0,0 +1,106 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +--- @alias AreaStruct +--- @class st.matter.clusters.ServiceArea.types.AreaStruct: st.matter.data_types.Structure +--- +--- @field public area_id st.matter.data_types.Uint32 +--- @field public map_id st.matter.data_types.Uint32 +--- @field public area_info ServiceArea.types.AreaInfoStruct +local AreaStruct = {} +local new_mt = StructureABC.new_mt({NAME = "AreaStruct", ID = data_types.name_to_id_map["Structure"]}) + +AreaStruct.field_defs = { + { + name = "area_id", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "map_id", + field_id = 1, + is_nullable = true, + is_optional = false, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "area_info", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "ServiceArea.types.AreaInfoStruct", + }, +} + +AreaStruct.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) + else + 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 + +AreaStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = AreaStruct.init +new_mt.__index.serialize = AreaStruct.serialize + +AreaStruct.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) 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(AreaStruct, new_mt) + +return AreaStruct + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/Feature.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/Feature.lua new file mode 100644 index 0000000000..e4878e14fd --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/Feature.lua @@ -0,0 +1,137 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class st.matter.clusters.ServiceArea.types.Feature +--- @alias Feature +--- +--- @field public SELECT_WHILE_RUNNING number 1 +--- @field public PROGRESS_REPORTING number 2 +--- @field public MAPS number 4 + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.SELECT_WHILE_RUNNING = 0x0001 +Feature.PROGRESS_REPORTING = 0x0002 +Feature.MAPS = 0x0004 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + SELECT_WHILE_RUNNING = 0x0001, + PROGRESS_REPORTING = 0x0002, + MAPS = 0x0004, +} + +--- @function Feature:is_select_while_running_set +--- @return boolean True if the value of SELECT_WHILE_RUNNING is non-zero +Feature.is_select_while_running_set = function(self) + return (self.value & self.SELECT_WHILE_RUNNING) ~= 0 +end + +--- @function Feature:set_select_while_running +--- Set the value of the bit in the SELECT_WHILE_RUNNING field to 1 +Feature.set_select_while_running = function(self) + if self.value ~= nil then + self.value = self.value | self.SELECT_WHILE_RUNNING + else + self.value = self.SELECT_WHILE_RUNNING + end +end + +--- @function Feature:unset_select_while_running +--- Set the value of the bits in the SELECT_WHILE_RUNNING field to 0 +Feature.unset_select_while_running = function(self) + self.value = self.value & (~self.SELECT_WHILE_RUNNING & self.BASE_MASK) +end +--- @function Feature:is_progress_reporting_set +--- @return boolean True if the value of PROGRESS_REPORTING is non-zero +Feature.is_progress_reporting_set = function(self) + return (self.value & self.PROGRESS_REPORTING) ~= 0 +end + +--- @function Feature:set_progress_reporting +--- Set the value of the bit in the PROGRESS_REPORTING field to 1 +Feature.set_progress_reporting = function(self) + if self.value ~= nil then + self.value = self.value | self.PROGRESS_REPORTING + else + self.value = self.PROGRESS_REPORTING + end +end + +--- @function Feature:unset_progress_reporting +--- Set the value of the bits in the PROGRESS_REPORTING field to 0 +Feature.unset_progress_reporting = function(self) + self.value = self.value & (~self.PROGRESS_REPORTING & self.BASE_MASK) +end +--- @function Feature:is_maps_set +--- @return boolean True if the value of MAPS is non-zero +Feature.is_maps_set = function(self) + return (self.value & self.MAPS) ~= 0 +end + +--- @function Feature:set_maps +--- Set the value of the bit in the MAPS field to 1 +Feature.set_maps = function(self) + if self.value ~= nil then + self.value = self.value | self.MAPS + else + self.value = self.MAPS + end +end + +--- @function Feature:unset_maps +--- Set the value of the bits in the MAPS field to 0 +Feature.unset_maps = function(self) + self.value = self.value & (~self.MAPS & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.SELECT_WHILE_RUNNING | + Feature.PROGRESS_REPORTING | + Feature.MAPS + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_select_while_running_set = Feature.is_select_while_running_set, + set_select_while_running = Feature.set_select_while_running, + unset_select_while_running = Feature.unset_select_while_running, + is_progress_reporting_set = Feature.is_progress_reporting_set, + set_progress_reporting = Feature.set_progress_reporting, + unset_progress_reporting = Feature.unset_progress_reporting, + is_maps_set = Feature.is_maps_set, + set_maps = Feature.set_maps, + unset_maps = Feature.unset_maps, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/LandmarkInfoStruct.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/LandmarkInfoStruct.lua new file mode 100644 index 0000000000..f23ca7e49b --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/LandmarkInfoStruct.lua @@ -0,0 +1,98 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" +--- @alias LandmarkInfoStruct +--- @class st.matter.clusters.ServiceArea.types.LandmarkInfoStruct: st.matter.data_types.Structure +--- +--- @field public landmark_tag Global.types.LandmarkTag +--- @field public relative_position_tag Global.types.RelativePositionTag +local LandmarkInfoStruct = {} +local new_mt = StructureABC.new_mt({NAME = "LandmarkInfoStruct", ID = data_types.name_to_id_map["Structure"]}) + +LandmarkInfoStruct.field_defs = { + { + name = "landmark_tag", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "Global.types.LandmarkTag", + }, + { + name = "relative_position_tag", + field_id = 1, + is_nullable = true, + is_optional = false, + data_type = require "Global.types.RelativePositionTag", + }, +} + +LandmarkInfoStruct.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) + else + 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 + +LandmarkInfoStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = LandmarkInfoStruct.init +new_mt.__index.serialize = LandmarkInfoStruct.serialize + +LandmarkInfoStruct.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) 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(LandmarkInfoStruct, new_mt) + +return LandmarkInfoStruct + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/SelectAreasStatus.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/SelectAreasStatus.lua new file mode 100644 index 0000000000..bda76258ca --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/SelectAreasStatus.lua @@ -0,0 +1,59 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +--- @class st.matter.clusters.ServiceArea.types.SelectAreasStatus: st.matter.data_types.Uint8 +--- @alias SelectAreasStatus +--- +--- @field public byte_length number 1 +--- @field public SUCCESS number 0 +--- @field public UNSUPPORTED_AREA number 1 +--- @field public INVALID_IN_MODE number 2 +--- @field public INVALID_SET number 3 + +local SelectAreasStatus = {} +local new_mt = UintABC.new_mt({NAME = "SelectAreasStatus", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.SUCCESS] = "SUCCESS", + [self.UNSUPPORTED_AREA] = "UNSUPPORTED_AREA", + [self.INVALID_IN_MODE] = "INVALID_IN_MODE", + [self.INVALID_SET] = "INVALID_SET", + } + 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.SUCCESS = 0x00 +new_mt.__index.UNSUPPORTED_AREA = 0x01 +new_mt.__index.INVALID_IN_MODE = 0x02 +new_mt.__index.INVALID_SET = 0x03 + +SelectAreasStatus.SUCCESS = 0x00 +SelectAreasStatus.UNSUPPORTED_AREA = 0x01 +SelectAreasStatus.INVALID_IN_MODE = 0x02 +SelectAreasStatus.INVALID_SET = 0x03 + +SelectAreasStatus.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(SelectAreasStatus, new_mt) + +return SelectAreasStatus + diff --git a/drivers/SmartThings/matter-rvc/src/ServiceArea/types/init.lua b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/init.lua new file mode 100644 index 0000000000..66b930bdd0 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/ServiceArea/types/init.lua @@ -0,0 +1,36 @@ +-- Copyright 2022 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. + +-- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. + +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("ServiceArea.types." .. key) + end + return types_mt.__types_cache[key] +end + +--- @class ServiceAreaTypes +--- +--- @field public SelectAreasStatus ServiceArea.types.SelectAreasStatus + +--- @field public Feature ServiceArea.types.Feature +local ServiceAreaTypes = {} + +setmetatable(ServiceAreaTypes, types_mt) + +return ServiceAreaTypes + diff --git a/drivers/SmartThings/matter-rvc/src/embedded_cluster_utils.lua b/drivers/SmartThings/matter-rvc/src/embedded_cluster_utils.lua new file mode 100644 index 0000000000..3c1f1bd084 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/embedded_cluster_utils.lua @@ -0,0 +1,49 @@ +local clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 13 then + clusters.ServiceArea = require "ServiceArea" +end + +local embedded_cluster_utils = {} + +local embedded_clusters = { + [clusters.ServiceArea.ID] = clusters.ServiceArea, +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + if embedded_clusters[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters[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 + table.sort(eps) + return eps + else + local eps = device:get_endpoints(cluster_id, opts) + table.sort(eps) + return eps + end +end + +return embedded_cluster_utils diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 7785f1a74a..28521bc928 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -15,6 +15,9 @@ local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" +local area_type = require "Global.types.AreaTypeTag" +local landmark = require "Global.types.LandmarkTag" +local embedded_cluster_utils = require "embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 10 local version = require "version" @@ -25,6 +28,11 @@ if version.api < 10 then clusters.OperationalState = require "OperationalState" end +if version.api < 13 then + clusters.ServiceArea = require "ServiceArea" + clusters.Global = require "Global" +end + local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" @@ -39,6 +47,10 @@ local subscribed_attributes = { [capabilities.robotCleanerOperatingState.ID] = { clusters.RvcOperationalState.attributes.OperationalState, clusters.RvcOperationalState.attributes.OperationalError + }, + [capabilities.serviceArea.ID] = { + clusters.ServiceArea.attributes.SupportedAreas, + clusters.ServiceArea.attributes.SelectedAreas } } @@ -55,10 +67,11 @@ local function device_added(driver, device) local run_mode_eps = device:get_endpoints(clusters.RvcRunMode.ID) or {} local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} local component_to_endpoint_map = { + ["main"] = run_mode_eps[1], ["runMode"] = run_mode_eps[1], ["cleanMode"] = clean_mode_eps[1] } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true} ) + device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) end local function device_init(driver, device) @@ -67,12 +80,18 @@ local function device_init(driver, device) end local function do_configure(driver, device) - local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) - if #clean_mode_eps == 0 then - local profile_name = "rvc" - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) + local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} + local service_area_eps = embedded_cluster_utils.get_endpoints(device, clusters.ServiceArea.ID) or {} + + local profile_name = "rvc" + if #clean_mode_eps > 0 then + profile_name = profile_name .. "-clean-mode" end + if #service_area_eps > 0 then + profile_name = profile_name .. "-service-area" + 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 local function info_changed(driver, device, event, args) @@ -324,6 +343,82 @@ local function rvc_operational_error_attr_handler(driver, device, ib, response) end end +local function upper_to_camelcase(name) + local name_camelcase = (string.lower(name)):gsub("_"," ") + name_camelcase = name_camelcase:gsub("(%l)(%w*)", + function(a, b) + return string.upper(a) .. b + end) + return name_camelcase +end + +local function rvc_service_area_supported_areas_handler(driver, device, ib, response) + local supported_areas = {} + for i, area in ipairs(ib.data.elements) do + if version.api < 13 then + clusters.ServiceArea.types.AreaStruct:augment_type(area) + clusters.ServiceArea.types.AreaInfoStruct:augment_type(area.elements.area_info) + if area.elements.area_info.elements.location_info.elements ~= nil then + clusters.Global.types.LocationDescriptorStruct:augment_type(area.elements.area_info.elements.location_info) + end + end + local area_id = area.elements.area_id.value + local location_info = area.elements.area_info.elements.location_info.elements + local landmark_info = area.elements.area_info.elements.landmark_info.elements + local area_name = "" + -- set the area name based on available location information + if location_info ~= nil then + if location_info.location_name.value ~= "" then + area_name = location_info.location_name.value + elseif location_info.floor_number.value ~= nil and location_info.area_type.value ~= nil then + area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + elseif location_info.floor_number.value ~= nil then + area_name = location_info.floor_number.value .. "F" + elseif location_info.area_type.value ~= nil then + area_name = upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + end + end + if area_name == "" then + area_name = upper_to_camelcase(string.gsub(landmark.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) + end + table.insert(supported_areas, {["areaId"] = area_id, ["areaName"] = area_name}) + end + -- Update Supported Areas + local component = device.profile.components["main"] + local event = capabilities.serviceArea.supportedAreas(supported_areas, {visibility = {displayed = false}}) + device:emit_component_event(component, event) +end + +-- in case selected area is not in supportedarea then should i add to supported area or remove from selectedarea +local function rvc_service_area_selected_areas_handler(driver, device, ib, response) + local selected_areas = {} + for i, areaId in ipairs(ib.data.elements) do + table.insert(selected_areas, areaId.value) + end + + local component = device.profile.components["main"] + local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) + device:emit_component_event(component, event) +end + +local function robot_cleaner_areas_selection_response_handler(driver, device, ib, response) + local select_areas_response = ib.info_block.data + if version.api < 13 then + clusters.ServiceArea.client.commands.SelectAreasResponse:augment_type(select_areas_response) + end + local status = select_areas_response.elements.status + local status_text = select_areas_response.elements.status_text + if status.value == clusters.ServiceArea.types.SelectAreasStatus.SUCCESS then + device.log.info(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) + else + device.log.error(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) + local selectedAreas = device:get_latest_state("main", capabilities.serviceArea.ID, capabilities.serviceArea.selectedAreas.NAME) + local component = device.profile.components["main"] + local event = capabilities.serviceArea.selectedAreas(selectedAreas, {state_change = true}) + device:emit_component_event(component, event) + end +end + -- Capability Handlers -- local function handle_robot_cleaner_mode(driver, device, cmd) device.log.info(string.format("handle_robot_cleaner_mode component: %s, mode: %s", cmd.component, cmd.args.mode)) @@ -350,6 +445,21 @@ local function handle_robot_cleaner_mode(driver, device, cmd) end end +local uint32_dt = require "st.matter.data_types.Uint32" +local function handle_robot_cleaner_areas_selection(driver, device, cmd) + + device.log.info(string.format("handle_robot_cleaner_areas_selection component: %s, serviceArea: %s", cmd.component, cmd.args.areas.value)) + + local selectAreas = clusters.ServiceArea.commands.SelectAreas(nil,nil,{}) + for i, areaId in ipairs(cmd.args.areas) do + table.insert(selectAreas, uint32_dt(areaId)) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + if cmd.component == "main" then + device:send(clusters.ServiceArea.commands.SelectAreas(device, endpoint_id, selectAreas)) + end +end + local matter_rvc_driver = { lifecycle_handlers = { init = device_init, @@ -371,6 +481,15 @@ local matter_rvc_driver = { [clusters.RvcOperationalState.attributes.OperationalState.ID] = rvc_operational_state_attr_handler, [clusters.RvcOperationalState.attributes.OperationalError.ID] = rvc_operational_error_attr_handler, }, + [clusters.ServiceArea.ID] = { + [clusters.ServiceArea.attributes.SupportedAreas.ID] = rvc_service_area_supported_areas_handler, + [clusters.ServiceArea.attributes.SelectedAreas.ID] = rvc_service_area_selected_areas_handler, + } + }, + cmd_response={ + [clusters.ServiceArea.ID] = { + [clusters.ServiceArea.client.commands.SelectAreasResponse.ID] = robot_cleaner_areas_selection_response_handler, + } } }, subscribed_attributes = subscribed_attributes, @@ -378,6 +497,9 @@ local matter_rvc_driver = { [capabilities.mode.ID] = { [capabilities.mode.commands.setMode.NAME] = handle_robot_cleaner_mode, }, + [capabilities.serviceArea.ID] = { + [capabilities.serviceArea.commands.selectAreas.NAME] = handle_robot_cleaner_areas_selection, + }, }, } 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 32a7229ba2..2198200f48 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -22,11 +22,13 @@ clusters.RvcCleanMode = require "RvcCleanMode" clusters.RvcOperationalState = require "RvcOperationalState" clusters.RvcRunMode = require "RvcRunMode" clusters.OperationalState = require "OperationalState" +clusters.ServiceArea = require "ServiceArea" +clusters.Global = require "Global" local APPLICATION_ENDPOINT = 10 local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("rvc-clean-mode.yml"), + profile = t_utils.get_profile_definition("rvc-clean-mode-service-area.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -47,6 +49,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.RvcRunMode.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RvcCleanMode.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RvcOperationalState.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ServiceArea.ID, cluster_type = "SERVER"}, }, device_types = { {device_type_id = 0x0074, device_type_revision = 1} -- Robot Vacuum Cleaner @@ -67,6 +70,10 @@ local function test_init() clusters.RvcOperationalState.attributes.OperationalState, clusters.RvcOperationalState.attributes.OperationalError, }, + [capabilities.serviceArea.ID] = { + clusters.ServiceArea.attributes.SupportedAreas, + clusters.ServiceArea.attributes.SelectedAreas + } } local subscribe_request = nil for _, attributes in pairs(subscribed_attributes) do @@ -793,4 +800,170 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +local locationDescriptorStruct = require "Global.types.LocationDescriptorStruct" +local areaInfoStruct = require "ServiceArea.types.AreaInfoStruct" +local areaStruct = require "ServiceArea.types.AreaStruct" +local landmarkInfoStruct = require "ServiceArea.types.LandmarkInfoStruct" + +test.register_message_test( + "Supported ServiceAreas must be registered", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ServiceArea.attributes.SupportedAreas:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + { + -- how to pass nil values here + areaStruct({ ["area_id"] = 0, ["map_id"] = 0, ["area_info"] = areaInfoStruct({ ["location_info"] = locationDescriptorStruct( {["location_name"] = "Location1", ["floor_number"] = 0, ["area_type"] = 0x04} ), ["landmark_info"] = landmarkInfoStruct({ ["landmark_tag"] = 0x1C, ["relative_position_tag"] = 0x02 })})}), + areaStruct({ ["area_id"] = 1, ["map_id"] = 0, ["area_info"] = areaInfoStruct({ ["location_info"] = locationDescriptorStruct( {["location_name"] = "", ["floor_number"] = 0, ["area_type"] = 0x04} ), ["landmark_info"] = landmarkInfoStruct({ ["landmark_tag"] = 0x1C, ["relative_position_tag"] = 0x02 })})}) + } + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.serviceArea.supportedAreas({ + {["areaId"] = 0, ["areaName"] = "Location1" }, + {["areaId"] = 1, ["areaName"] = "0F Balcony" }, + }, { visibility = { displayed = false } })) + }, + } +) + +local uint32_dt = require "st.matter.data_types.Uint32" +local selectAreasStatus = require "ServiceArea.types.SelectAreasStatus" + +test.register_message_test( + "ServiceArea attribute report must emit appropriate capability event", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ServiceArea.attributes.SelectedAreas:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.ServiceArea.attributes.SelectedAreas({uint32_dt(1),uint32_dt(2),uint32_dt(5)}) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.serviceArea.selectedAreas({ 1,2,5 }, { visibility = { displayed = false } })) + }, + } +) + +local uint32_dt = require "st.matter.data_types.Uint32" + +test.register_message_test( + "Select ServiceAreas command must trigger appropriate matter cluster", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "serviceArea", component = "main", command = "selectAreas", args = { {1, 2} } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ServiceArea.server.commands.SelectAreas(mock_device, APPLICATION_ENDPOINT, {uint32_dt(1),uint32_dt(2)}) + } + }, + } +) + + +test.register_message_test( + "Selected ServiceAreasResponse must log Success status or emit last valid selectedAreas capability on non-Success status", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "serviceArea", component = "main", command = "selectAreas", args = { {1, 2, 5} } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ServiceArea.server.commands.SelectAreas(mock_device, APPLICATION_ENDPOINT, {uint32_dt(1),uint32_dt(2),uint32_dt(5)}) --0 is the index where Clean Mode 1 is stored. + } + }, + -- success response + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ServiceArea.client.commands.SelectAreasResponse:build_test_command_response(mock_device, APPLICATION_ENDPOINT, + selectAreasStatus.SUCCESS, "Success Response") + } + }, + -- Attribute report for last successful selectArea command + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ServiceArea.attributes.SelectedAreas:build_test_report_data(mock_device, APPLICATION_ENDPOINT, + clusters.ServiceArea.attributes.SelectedAreas({uint32_dt(1),uint32_dt(2),uint32_dt(5)}) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.serviceArea.selectedAreas({ 1,2,5 }, { visibility = { displayed = false } })) + }, + -- invalid command assuming area id 6 is not supported + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "serviceArea", component = "main", command = "selectAreas", args = { {1, 2, 6} } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ServiceArea.server.commands.SelectAreas(mock_device, APPLICATION_ENDPOINT, {uint32_dt(1),uint32_dt(2),uint32_dt(6)}) + } + }, + -- error response logs error and re-emits last valid 1,2,5 + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ServiceArea.client.commands.SelectAreasResponse:build_test_command_response(mock_device, APPLICATION_ENDPOINT, + selectAreasStatus.UNSUPPORTED_AREA, "AreaId 6 is not supported") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.serviceArea.selectedAreas({ 1,2,5 },{ state_change=true})) + }, + } +) + +test.run_registered_tests() From 90f2a5f4a418f9f67eda18670013760a422d97ef Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 28 May 2025 12:15:02 -0500 Subject: [PATCH 010/449] Matter Thermostat: Use larger temperature range (#2130) For RPC >= 5, the hub converts temperature values from capability commands to Celsius, meaning the driver no longer needs to use separate ranges for Celsius and Fahrenheit in order to determine the unit as Celsius can always be assumed. A range is still used, in order to filter out values received from the device that likely to be erroneous, but it is much larger and will be able to accommodate the temperature range for most use cases. --- .../SmartThings/matter-thermostat/src/init.lua | 15 ++++++++------- .../test/test_matter_thermo_setpoint_limits.lua | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 3b11d3c60e..c85e3eed2e 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -116,20 +116,21 @@ local SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_wi local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local MGM3_PPM_CONVERSION_FACTOR = 24.45 --- This is a work around to handle when units for temperatureSetpoint is changed for the App. +-- For RPC version >= 6, we can always assume that the values received from temperatureSetpoint +-- are in Celsius, but we still limit the setpoint range to somewhat reasonable values. +-- For RPC <= 5, this is a work around to handle when units for temperatureSetpoint is changed for the App. -- When units are switched, we will never know the units of the received command value as the arguments don't contain the unit. -- So to handle this we assume the following ranges considering usual thermostat/water-heater temperatures: -- Thermostat: -- 1. if the received setpoint command value is in range 5 ~ 40, it is inferred as *C -- 2. if the received setpoint command value is in range 41 ~ 104, it is inferred as *F -local THERMOSTAT_MAX_TEMP_IN_C = 40.0 -local THERMOSTAT_MIN_TEMP_IN_C = 5.0 - +local THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 +local THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 -- Water Heater: -- 1. if the received setpoint command value is in range 30 ~ 80, it is inferred as *C -- 2. if the received setpoint command value is in range 86 ~ 176, it is inferred as *F -local WATER_HEATER_MAX_TEMP_IN_C = 80.0 -local WATER_HEATER_MIN_TEMP_IN_C = 30.0 +local WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 +local WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 local setpoint_limit_device_field = { MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", @@ -1789,7 +1790,7 @@ local function set_setpoint(setpoint) MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C end local value = cmd.args.setpoint - if (value > MAX_TEMP_IN_C) then -- assume this is a fahrenheit value + if version.rpc <= 5 and value > MAX_TEMP_IN_C then value = utils.f_to_c(value) end 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 50d9d7e808..b28d11dcc7 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 @@ -1,3 +1,4 @@ +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -15,7 +16,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -93,7 +93,7 @@ local function test_init() read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) - test.set_rpc_version(5) + test.set_rpc_version(6) end test.set_test_init_function(test_init) @@ -120,13 +120,13 @@ local function configure(device) clusters.Thermostat.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 1, 2667) --26.67 celcius }) test.socket.capability:__expect_send( - device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 5.00, maximum = 40.00, step = 0.1 }, unit = "C" })) + device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) ) test.socket.capability:__expect_send( device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({ value = 24.44, unit = "C" })) ) test.socket.capability:__expect_send( - device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 5.00, maximum = 40.00, step = 0.1 }, unit = "C" })) + device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) ) test.socket.capability:__expect_send( device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 26.67, unit = "C" })) From e05da43cafab4c392a74b792b5a359d29f04c36f Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 29 May 2025 15:16:49 -0500 Subject: [PATCH 011/449] Matter Switch: Remove Energy Setters in added (#2152) * remove energy setters from added --- .../SmartThings/matter-switch/src/init.lua | 8 ------- .../src/test/test_electrical_sensor.lua | 21 ------------------- 2 files changed, 29 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 8d99e62ccb..c4e9d6dceb 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1318,14 +1318,6 @@ local function device_added(driver, device) device:send(req) end - -- Reset the values - if device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - end - if device:supports_capability(capabilities.energyMeter) then - device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) - end - -- call device init in case init is not called after added due to device caching device_init(driver, device) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 6cca6a8cd0..e812bdfdaa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -162,27 +162,6 @@ local function test_init_periodic() test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") end -test.register_coroutine_test( - "Check the power and energy meter when the device is added", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - 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" })) - ) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.wait_for_events() - end -) - test.register_message_test( "On command should send the appropriate commands", { From 7b94e3315c9988b1f8011e1c60c2c59261be229f Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Fri, 30 May 2025 15:42:28 -0500 Subject: [PATCH 012/449] Add reconfiguration for zigbee power meter to reduce reporting This will check devices on init to see if they support the clusters for power reporting and if so schedules a 5 minute timer to check all devices in the driver, and attempt to configure them to a lower reporting frequency. It will then mark the configuration version on the device to avoid it happening again. --- .../src/aqara/multi-switch/init.lua | 3 +- .../zigbee-switch/src/configurations.lua | 92 ++++++++++++ .../zigbee-switch/src/ezex/init.lua | 3 +- .../zigbee-switch/src/hanssem/init.lua | 3 +- .../SmartThings/zigbee-switch/src/init.lua | 16 ++- .../src/inovelli-vzm31-sn/init.lua | 3 +- .../zigbee-switch/src/laisiao/init.lua | 3 +- .../src/multi-switch-no-master/init.lua | 3 +- .../zigbee-switch/src/robb/init.lua | 3 +- .../test/test_all_capability_zigbee_bulb.lua | 133 ++++++++++++++++++ .../src/test/test_aqara_smart_plug.lua | 2 + .../src/test/test_aqara_smart_plug_t1.lua | 2 + .../src/test/test_aurora_relay.lua | 1 + .../test/test_enbrighten_metering_dimmer.lua | 1 + .../src/test/test_jasco_switch.lua | 1 + .../src/test/test_multi_switch_power.lua | 84 +++++++++++ .../test/test_robb_smarrt_2-wire_dimmer.lua | 1 + .../src/test/test_robb_smarrt_knob_dimmer.lua | 1 + .../src/test/test_switch_power.lua | 1 + .../test/test_zigbee_dimmer_power_energy.lua | 1 + .../zigbee-switch/src/wallhero/init.lua | 3 +- .../enbrighten-metering-dimmer/init.lua | 3 +- .../src/zigbee-dimming-light/init.lua | 3 +- .../src/zigbee-dual-metering-switch/init.lua | 3 +- .../init.lua | 3 +- .../ikea-xy-color-bulb/init.lua | 2 +- 26 files changed, 360 insertions(+), 14 deletions(-) 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 1cfb93c529..1bb5ff742c 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -2,6 +2,7 @@ local device_lib = require "st.device" local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local configurations = require "configurations" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 @@ -100,7 +101,7 @@ end local aqara_multi_switch_handler = { NAME = "Aqara Multi Switch Handler", lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), added = device_added }, can_handle = is_aqara_products diff --git a/drivers/SmartThings/zigbee-switch/src/configurations.lua b/drivers/SmartThings/zigbee-switch/src/configurations.lua index 828c223d67..ea33710ce1 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations.lua @@ -14,9 +14,15 @@ local clusters = require "st.zigbee.zcl.clusters" local constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" +local device_def = require "st.device" local ColorControl = clusters.ColorControl local IASZone = clusters.IASZone +local Status = require "st.zigbee.generated.types.ZclStatus" + +local CONFIGURATION_VERSION_KEY = "_configuration_version" +local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" local devices = { IKEA_RGB_BULB = { @@ -60,8 +66,94 @@ local devices = { } } + local configurations = {} +local active_power_configuration = { + cluster = clusters.ElectricalMeasurement.ID, + attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 5 +} + +local instantaneous_demand_configuration = { + cluster = clusters.SimpleMetering.ID, + attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 5 +} + +configurations.check_and_reconfig_devices = function(driver) + for device_id, device in pairs(driver.device_cache) do + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if device:supports_capability(capabilities.powerMeter) then + if device:supports_server_cluster(clusters.ElectricalMeasurement.ID) then + -- Increase minimum reporting interval to 5 seconds + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:configure_reporting(device, 5, 600, 5)) + device:add_configured_attribute(active_power_configuration) + end + if device:supports_server_cluster(clusters.SimpleMetering.ID) then + -- Increase minimum reporting interval to 5 seconds + device:send(clusters.SimpleMetering.attributes.InstantaneousDemand:configure_reporting(device, 5, 600, 5)) + device:add_configured_attribute(instantaneous_demand_configuration) + end + end + device:set_field(CONFIGURATION_ATTEMPTED, true, {persist = true}) + end + end + driver._reconfig_timer = nil +end + +configurations.handle_reporting_config_response = function(driver, device, zb_mess) + local dev = device + local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) + if find_child_fn ~= nil then + local child = find_child_fn(device, zb_mess.address_header.src_endpoint.value) + if child ~= nil then + dev = child + end + end + if dev:get_field(CONFIGURATION_ATTEMPTED) == true then + if zb_mess.body.zcl_body.global_status ~= nil and zb_mess.body.zcl_body.global_status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + elseif zb_mess.body.zcl_body.config_records ~= nil then + local config_records = zb_mess.body.zcl_body.config_records + for _, record in ipairs(config_records) do + if zb_mess.address_header.cluster.value == clusters.SimpleMetering.ID then + if record.attr_id.value == clusters.SimpleMetering.attributes.InstantaneousDemand.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + elseif zb_mess.address_header.cluster.value == clusters.ElectricalMeasurement.ID then + if record.attr_id.value == clusters.ElectricalMeasurement.attributes.ActivePower.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + end + + end + end + end +end + +configurations.power_reconfig_wrapper = function(orig_function) + local new_init = function(driver, device) + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if driver._reconfig_timer == nil then + driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") + end + end + orig_function(driver, device) + end + return new_init +end + configurations.get_device_configuration = function(zigbee_device) for _, device in pairs(devices) do for _, fingerprint in pairs(device.FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua index 406d51ccd7..f88c39bb75 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua @@ -13,6 +13,7 @@ -- limitations under the License. local zigbee_constants = require "st.zigbee.constants" +local configurations = require "configurations" local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { { model = "E240-KR116Z-HA" } @@ -37,7 +38,7 @@ end local ezex_switch_handler = { NAME = "ezex switch handler", lifecycle_handlers = { - init = do_init + init = configurations.power_reconfig_wrapper(do_init) }, can_handle = is_zigbee_ezex_switch } diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua index e5ba00b77f..bb845984ca 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua @@ -13,6 +13,7 @@ -- limitations under the License. local stDevice = require "st.device" +local configurations = require "configurations" local FINGERPRINTS = { { mfr = "Winners", model = "LSS1-101", children = 0 }, @@ -78,7 +79,7 @@ local HanssemSwitch = { NAME = "Zigbee Hanssem Switch", lifecycle_handlers = { added = device_added, - init = device_init + init = configurations.power_reconfig_wrapper(device_init) }, can_handle = can_handle_hanssem_switch } diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 8ff05007b4..85efef5e11 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -17,7 +17,9 @@ local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local clusters = require "st.zigbee.zcl.clusters" local configurationMap = require "configurations" +local zcl_global_commands = require "st.zigbee.zcl.global_commands" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement local preferences = require "preferences" local device_lib = require "st.device" @@ -127,6 +129,7 @@ local function device_added(driver, device, event) end end + local zigbee_switch_driver_template = { supported_capabilities = { capabilities.switch, @@ -164,8 +167,19 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi") }, + zigbee_handlers = { + global = { + [SimpleMetering.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + }, + [ElectricalMeasurement.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + } + } + }, + current_config_version = 1, lifecycle_handlers = { - init = device_init, + init = configurationMap.power_reconfig_wrapper(device_init), added = device_added, infoChanged = info_changed, doConfigure = do_configure diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua index 29373c2b75..d6d094209c 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua @@ -19,6 +19,7 @@ local st_device = require "st.device" local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" +local configurations = require "configurations" local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" @@ -358,7 +359,7 @@ local inovelli_vzm31_sn = { NAME = "inovelli vzm31-sn handler", lifecycle_handlers = { doConfigure = do_configure, - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), infoChanged = info_changed }, zigbee_handlers = { diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua index f8608ac038..edb829bf1f 100755 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua @@ -14,6 +14,7 @@ local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" +local configurations = require "configurations" local FINGERPRINTS = { { mfr = "LAISIAO", model = "yuba" }, @@ -70,7 +71,7 @@ local laisiao_bath_heater = { capabilities.switch, }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), }, capability_handlers = { [capabilities.switch.ID] = { diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua index c6fd1180a6..bec5b1d87d 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua @@ -13,6 +13,7 @@ -- limitations under the License. local st_device = require "st.device" local utils = require "st.utils" +local configurations = require "configurations" local MULTI_SWITCH_NO_MASTER_FINGERPRINTS = { { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, @@ -113,7 +114,7 @@ end local multi_switch_no_master = { NAME = "multi switch no master", lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), added = device_added }, can_handle = is_multi_switch_no_master diff --git a/drivers/SmartThings/zigbee-switch/src/robb/init.lua b/drivers/SmartThings/zigbee-switch/src/robb/init.lua index c00488ee30..4593b4339f 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/init.lua @@ -15,6 +15,7 @@ local constants = require "st.zigbee.constants" local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" +local configurations = require "configurations" local SimpleMetering = zcl_clusters.SimpleMetering local ROBB_DIMMER_FINGERPRINTS = { @@ -57,7 +58,7 @@ local robb_dimmer_handler = { } }, lifecycle_handlers = { - init = do_init + init = configurations.power_reconfig_wrapper(do_init) }, can_handle = is_robb_dimmer } 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 70f815f303..ed4ca57da3 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 @@ -22,6 +22,13 @@ local ElectricalMeasurement = clusters.ElectricalMeasurement local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" + local zigbee_bulb_all_caps = { components = { @@ -43,12 +50,43 @@ local mock_device = test.mock_device.build_test_zigbee_device({ profile = zigbee zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) zigbee_test_utils.init_noop_health_check_timer() end test.set_test_init_function(test_init) +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + + test.register_message_test( "Reported level should be handled", { @@ -369,12 +407,107 @@ test.register_coroutine_test( end, { test_init = function() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") end } ) +test.register_coroutine_test( + "configuration version below 1", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 config response not success", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == nil) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records ElectricalMeasurement", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, nil, ElectricalMeasurement.attributes.ActivePower.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records SimpleMetering", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, nil, SimpleMetering.attributes.InstantaneousDemand.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + test.register_coroutine_test( "set color command test", function() 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 ea51561f25..805aa23d95 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 @@ -70,7 +70,9 @@ local mock_standard = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) + mock_standard:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_standard) zigbee_test_utils.init_noop_health_check_timer() end 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 f8f4c6a88a..e0fc03064d 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 @@ -71,7 +71,9 @@ local mock_standard = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) + mock_standard:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_standard) zigbee_test_utils.init_noop_health_check_timer() end 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 ee8e015b8f..6dd6ed75b7 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua @@ -35,6 +35,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ }) local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) end 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 b225cf5dac..b7b4f67b7a 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 @@ -34,6 +34,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ }) local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) end 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 ebaeba8937..6b07420b87 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -33,6 +33,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ }) local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) end 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 fc957ec750..740a7ee590 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 @@ -20,6 +20,12 @@ local ElectricalMeasurement = clusters.ElectricalMeasurement local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" local profile = t_utils.get_profile_definition("switch-power-smartplug.yml") @@ -64,14 +70,92 @@ local mock_child_device = test.mock_device.build_test_child_device({ zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_base_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_base_device) + mock_parent_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_parent_device) + mock_child_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_child_device) zigbee_test_utils.init_noop_health_check_timer() end test.set_test_init_function(test_init) +local function build_config_response_msg(device, cluster, status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body = config_reporting_response.ConfigureReportingResponse({}, status) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_coroutine_test( + "configuration version below 1", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) + assert(mock_parent_device:get_field("_configuration_version") == nil) + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "init" }) + assert(mock_child_device:get_field("_configuration_version") == nil) + test.socket.device_lifecycle:__queue_receive({ mock_child_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({mock_parent_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_parent_device, 5, 600, 5)}) + test.socket.zigbee:__expect_send({mock_parent_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_parent_device, 5, 600, 5):to_endpoint(0x02)}) + test.mock_time.advance_time(50 * 60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_parent_device.id, build_config_response_msg(mock_parent_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_parent_device.id, build_config_response_msg(mock_parent_device, ElectricalMeasurement.ID, Status.SUCCESS):from_endpoint(0x02)}) + test.wait_for_events() + assert(mock_child_device:get_field("_configuration_version") == 1, "config version for child should be 1") + assert(mock_parent_device:get_field("_configuration_version") == 1, "config version for parent should be 1") + end, + { + test_init = function() + -- no op to avoid auto device add and immediate init event on driver startup + end + } +) + +test.register_coroutine_test( + "configuration version at 1 doesn't reconfigure", + function() + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) + mock_child_device:set_field("_configuration_version", 1, {persist = true}) + mock_parent_device:set_field("_configuration_version", 1, {persist = true}) + assert(mock_parent_device:get_field("_configuration_version") == 1) + assert(mock_child_device:get_field("_configuration_version") == 1) + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_child_device.id, "init" }) + test.wait_for_events() + assert(mock_child_device:get_field("_configuration_version") == 1) + assert(mock_parent_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to avoid auto device add and immediate init event on driver startup + end + } +) + test.register_message_test( "Refresh on parent device should read all necessary attributes", { 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 daf3cc464f..6a742aa472 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 @@ -39,6 +39,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) zigbee_test_utils.init_noop_health_check_timer() end 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 5d6d459ef3..cbea45e392 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 @@ -39,6 +39,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) zigbee_test_utils.init_noop_health_check_timer() end 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 89cf85e3bb..4115546f55 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -38,6 +38,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_test_utils.prepare_zigbee_env_info() local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua index 3d5da8dceb..9b62d57546 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua @@ -32,6 +32,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ }) local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua index fc0bf6abfd..79f3110e41 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua @@ -18,6 +18,7 @@ local stDevice = require "st.device" local zcl_clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local configurations = require "configurations" local Scenes = zcl_clusters.Scenes local PRIVATE_CLUSTER_ID = 0x0006 @@ -130,7 +131,7 @@ local wallheroswitch = { NAME = "Zigbee Wall Hero Switch", lifecycle_handlers = { added = device_added, - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), infoChanged = device_info_changed }, zigbee_handlers = { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua index 936e04f150..d4fcdb3990 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua @@ -16,6 +16,7 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" local SimpleMetering = clusters.SimpleMetering +local configurations = require "configurations" local ENBRIGHTEN_METERING_DIMMER_FINGERPRINTS = { { mfr = "Jasco Products", model = "43082" } @@ -57,7 +58,7 @@ local enbrighten_metering_dimmer = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure }, can_handle = is_enbrighten_metering_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 7395823a2e..153c8325e8 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -14,6 +14,7 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" +local configurations = require "configurations" local OnOff = clusters.OnOff local Level = clusters.Level @@ -97,7 +98,7 @@ end local zigbee_dimming_light = { NAME = "Zigbee Dimming Light", lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), added = device_added }, sub_drivers = { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua index 3a952cb32e..68f7d63675 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua @@ -17,6 +17,7 @@ local clusters = require "st.zigbee.zcl.clusters" local OnOff = clusters.OnOff local ElectricalMeasurement = clusters.ElectricalMeasurement local utils = require "st.utils" +local configurations = require "configurations" local CHILD_ENDPOINT = 2 @@ -76,7 +77,7 @@ local zigbee_dual_metering_switch = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), added = device_added }, can_handle = can_handle_zigbee_dual_metering_switch diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 4234fd36ab..044bf509df 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -16,6 +16,7 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local zigbee_constants = require "st.zigbee.constants" local energy_meter_defaults = require "st.zigbee.defaults.energyMeter_defaults" +local configurations = require "configurations" local SimpleMetering = clusters.SimpleMetering @@ -50,7 +51,7 @@ local zigbee_metering_plug_power_conumption_report = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure }, can_handle = function(opts, driver, device, ...) diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua index dcabcd1811..cf18d2ff82 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua @@ -170,7 +170,7 @@ end local ikea_xy_color_bulb = { NAME = "IKEA XY Color Bulb", lifecycle_handlers = { - init = device_init + init = configurationMap.power_reconfig_wrapper(device_init) }, capability_handlers = { [capabilities.colorControl.ID] = { From e5663b69ec2d80f6202629076d1b7570b02ca2f3 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:54:03 -0500 Subject: [PATCH 013/449] Add new profiles including thermostatOperatingState (#2167) Adding new profiles for thermostats with no battery after support for thermostatOperatingState was added. Previously, devices would use `thermostat-heating-only-nostate-nobattery` (for example) whether or not they supported ThermostatRunningState. --- .../thermostat-cooling-only-nobattery.yml | 24 +++++++++++++++++ .../thermostat-heating-only-nobattery.yml | 24 +++++++++++++++++ .../profiles/thermostat-nobattery.yml | 27 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/thermostat-cooling-only-nobattery.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/thermostat-heating-only-nobattery.yml create mode 100644 drivers/SmartThings/matter-thermostat/profiles/thermostat-nobattery.yml diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-cooling-only-nobattery.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-cooling-only-nobattery.yml new file mode 100644 index 0000000000..45858cfdc9 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-cooling-only-nobattery.yml @@ -0,0 +1,24 @@ +name: thermostat-cooling-only-nobattery +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatMode + version: 1 + - id: thermostatCoolingSetpoint + version: 1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - idle + - cooling + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-heating-only-nobattery.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-heating-only-nobattery.yml new file mode 100644 index 0000000000..a5c946b6c1 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-heating-only-nobattery.yml @@ -0,0 +1,24 @@ +name: thermostat-heating-only-nobattery +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatMode + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - idle + - heating + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-nobattery.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-nobattery.yml new file mode 100644 index 0000000000..ecbd592df7 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-nobattery.yml @@ -0,0 +1,27 @@ +name: thermostat-nobattery +components: +- id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatMode + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + - id: thermostatCoolingSetpoint + version: 1 + - id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - idle + - cooling + - heating + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat From c7ebaf1d31522f6458643edcdabeafa2851c430b Mon Sep 17 00:00:00 2001 From: Huangxiangjie Date: Wed, 11 Jun 2025 22:27:07 +0800 Subject: [PATCH 014/449] Add air purifier profile (#2165) * Add air purifier profile --------- Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --- ...ier-hepa-wind-aqs-pm25-meas-pm25-level.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml new file mode 100644 index 0000000000..3f014f0b91 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml @@ -0,0 +1,32 @@ +name: air-purifier-hepa-wind-aqs-pm25-meas-pm25-level +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: airQualityHealthConcern + version: 1 + - id: fineDustSensor + version: 1 + - id: fineDustHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa Filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier \ No newline at end of file From 0f8bf52c6ef0ad5ebb7c9ab7d2a745e62c8a4f9e Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:07:44 -0500 Subject: [PATCH 015/449] Matter Sensor: Add motion-illuminance-temperature-humidity-battery profile and fingerprint (#2191) * add new sensor fingerprint and profiles (battery/batteryLevel versions) --- .../matter-sensor/fingerprints.yml | 8 ++++++ ...luminance-temperature-humidity-battery.yml | 25 +++++++++++++++++++ ...ance-temperature-humidity-batteryLevel.yml | 25 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml create mode 100644 drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 65cd70826e..a5ab7a5e2e 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -205,6 +205,14 @@ matterGeneric: - id: 0x0106 # Light Sensor - id: 0x0302 # Temperature Sensor deviceProfileName: motion-illuminance-temperature-battery + - id: "matter/motion-illum-temp-humidity" + deviceLabel: Matter Motion/Illuminance Sensor + deviceTypes: + - id: 0x0107 # Occupancy Sensor + - id: 0x0106 # Light Sensor + - id: 0x0302 # Temperature Sensor + - id: 0x0307 # Humidity Sensor + deviceProfileName: motion-illuminance-temperature-humidity-battery - id: "matter/motion-sensor" deviceLabel: Matter Motion Sensor deviceTypes: diff --git a/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml new file mode 100644 index 0000000000..689f0c1eac --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml @@ -0,0 +1,25 @@ +name: motion-illuminance-temperature-humidity-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml new file mode 100644 index 0000000000..00708e6430 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml @@ -0,0 +1,25 @@ +name: motion-illuminance-temperature-humidity-batteryLevel +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true From ec837c9f5af1ff012cdcac7227cdfd6f9291b533 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Fri, 6 Jun 2025 10:32:47 -0500 Subject: [PATCH 016/449] philips-hue: Refresh device on online sse update --- .../philips-hue/src/handlers/attribute_emitters.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index 736d79af73..f274ef37f0 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -175,6 +175,11 @@ function AttributeEmitters.connectivity_update(child_device, zigbee_status) child_device.log.info_with({hub_logs=true}, "Device zigbee status event, marking device online") child_device:online() child_device:set_field(Fields.IS_ONLINE, true) + child_device.driver:inject_capability_command(child_device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) elseif zigbee_status.status == "connectivity_issue" then child_device.log.info_with({hub_logs=true}, "Device zigbee status event, marking device offline") child_device:set_field(Fields.IS_ONLINE, false) From ce35126b27315baa72c297219e51386a37ec84cf Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:26:57 +0900 Subject: [PATCH 017/449] [Aqara] Aqara wall switch (no neutral, single) - fix the button function configuration (#2143) * The issue related to the button function configuration of the Aqara wall switch (no neutral, single) has been fixed. * Duplicate card registration issue fixed. * Adjust Testcase --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 2 +- drivers/SmartThings/zigbee-switch/src/aqara/init.lua | 1 + .../SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 2d938fcf11..d45c0aa13c 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -75,7 +75,7 @@ zigbeeManufacturer: deviceLabel: Aqara Smart Wall Switch (No Neutral, Single Rocker) manufacturer: LUMI model: lumi.switch.b1laus01 - deviceProfileName: basic-switch + deviceProfileName: aqara-switch-child - id: "LUMI/lumi.switch.b2laus01" deviceLabel: Aqara Smart Wall Switch (No Neutral, Double Rocker) 1 manufacturer: LUMI diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua index fba93dd85e..6558f7f282 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua @@ -33,6 +33,7 @@ local FINGERPRINTS = { { 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" }, 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 1bb5ff742c..1c1431f518 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -12,6 +12,7 @@ local FINGERPRINTS = { { 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" }, From 48b63ced34c34a97316e062a5fbb8b155838a449 Mon Sep 17 00:00:00 2001 From: GAFfrient <96058156+GAFfrient@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:51:31 +0200 Subject: [PATCH 018/449] Initial version, adding support for frient Motion Sensor/Pro (#2135) * Initial version, adding support for frient Motion Sensor/Pro * Fix 1 according to test results * Fix 2: Deprecated test. These devices are now covered by test_frient_motion_sensor.lua and test_frient_motion_sensor_pro.lua * Fix 3 - restoring previous format * Fix 4 - according to feedback --- .../zigbee-motion-sensor/fingerprints.yml | 6 +- .../profiles/frient-motion-battery.yml | 42 ++ ...motion-temp-illuminance-tamper-battery.yml | 59 +++ .../zigbee-motion-sensor/src/frient/init.lua | 228 +++++++-- .../zigbee-motion-sensor/src/init.lua | 4 +- .../src/test/test_frient_motion_sensor.lua | 274 +++++++++++ .../test/test_frient_motion_sensor_pro.lua | 440 ++++++++++++++++++ .../src/test/test_frient_sensor.lua | 175 ------- 8 files changed, 1005 insertions(+), 223 deletions(-) create mode 100644 drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-battery.yml create mode 100644 drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-tamper-battery.yml create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua delete mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_sensor.lua diff --git a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml index 2037992e9c..c1a3b0fda3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml @@ -159,15 +159,15 @@ zigbeeManufacturer: model: VMS_ADUROLIGHT deviceProfileName: motion-battery - id: frientA/S/140 - deviceLabel: frient Motion Sensor + deviceLabel: frient Motion Sensor Pro manufacturer: frient A/S model: MOSZB-140 - deviceProfileName: motion-temp-battery + deviceProfileName: frient-motion-temp-illuminance-tamper-battery - id: frientA/S/141 deviceLabel: frient Motion Sensor manufacturer: frient A/S model: MOSZB-141 - deviceProfileName: motion-battery + deviceProfileName: frient-motion-battery - id: Compacta/ZBMS3-1 deviceLabel: Smartenit Motion Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-battery.yml b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-battery.yml new file mode 100644 index 0000000000..d23907609d --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-battery.yml @@ -0,0 +1,42 @@ +name: frient-motion-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - title: "Motion Turn-Off Delay (s)" + name: occupiedToUnoccupiedD + description: "Delay in seconds to report after no motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + - title: "Motion Turn-On Delay (s)" + name: unoccupiedToOccupiedD + description: "Delay in seconds to report after motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 0 + - title: "Movement Threshold in Turn-On Delay" + name: unoccupiedToOccupiedT + description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 254 + default: 1 diff --git a/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-tamper-battery.yml b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-tamper-battery.yml new file mode 100644 index 0000000000..f347d0148d --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-tamper-battery.yml @@ -0,0 +1,59 @@ +name: frient-motion-temp-illuminance-tamper-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Motion Turn-Off Delay (s)" + name: occupiedToUnoccupiedD + description: "Delay in seconds to report after no motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + - title: "Motion Turn-On Delay (s)" + name: unoccupiedToOccupiedD + description: "Delay in seconds to report after motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 0 + - title: "Movement Threshold in Turn-On Delay" + name: unoccupiedToOccupiedT + description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 254 + default: 1 diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua index 421fef60ae..0278350cbe 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua @@ -12,72 +12,214 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- ZCL +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" -local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local device_management = require "st.zigbee.device_management" + +local IASZone = zcl_clusters.IASZone +local IlluminanceMeasurement = zcl_clusters.IlluminanceMeasurement local OccupancySensing = zcl_clusters.OccupancySensing local PowerConfiguration = zcl_clusters.PowerConfiguration -local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement -local capabilities = require "st.capabilities" +local BATTERY_MIN_VOLTAGE = 2.3 +local BATTERY_MAX_VOLTAGE = 3.0 +local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0 -local FRIENT_TEMP_CONFIG = { - minimum_interval = 30, - maximum_interval = 300, - reportable_change = 100, - endpoint = 0x26 -} +local OCCUPANCY_ENDPOINT = 0x22 +local TAMPER_ENDPOINT = 0x23 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local TEMPERATURE_ENDPOINT = 0x26 +local ILLUMINANCE_ENDPOINT = 0x27 -local FRIENT_BATTERY_CONFIG = { - minimum_interval = 30, - maximum_interval = 21600, - reportable_change = 1, - endpoint = 0x23 +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "MOSZB-140"}, + { mfr = "frient A/S", model = "MOSZB-141"} } +local function can_handle_frient_motion_sensor(opts, driver, device) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + local function occupancy_attr_handler(driver, device, occupancy, zb_rx) - device:emit_event( - occupancy.value == 1 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive() - ) + device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + device:emit_event(zone_status:is_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear()) +end + +local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx) + generate_event_from_zone_status(driver, device, attr_val, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + + +local CONFIGURATIONS = { + [OCCUPANCY_ENDPOINT] = { + cluster = OccupancySensing.ID, + attribute = OccupancySensing.attributes.Occupancy.ID, + data_type = OccupancySensing.attributes.Occupancy.base_type, + minimum_interval = 0, + maximum_interval = 3600, + endpoint = OCCUPANCY_ENDPOINT + }, + [TAMPER_ENDPOINT] = { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 30, + maximum_interval = 300, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1, + endpoint = TAMPER_ENDPOINT + }, + [TEMPERATURE_ENDPOINT] = { + cluster = TemperatureMeasurement.ID, + attribute = TemperatureMeasurement.attributes.MeasuredValue.ID, + minimum_interval = 30, + maximum_interval = 3600, + data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type, + reportable_change = 10, + endpoint = TEMPERATURE_ENDPOINT + }, + [ILLUMINANCE_ENDPOINT] = { + cluster = IlluminanceMeasurement.ID, + attribute = IlluminanceMeasurement.attributes.MeasuredValue.ID, + data_type = IlluminanceMeasurement.attributes.MeasuredValue.base_type, + minimum_interval = 10, + maximum_interval = 3600, + reportable_change = 0x2711, + endpoint = ILLUMINANCE_ENDPOINT + } +} + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device) + + local attribute + attribute = CONFIGURATIONS[OCCUPANCY_ENDPOINT] + -- binding is directly triggered for specific endpoint in do_configure + device:add_monitored_attribute(attribute) + + if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then + attribute = CONFIGURATIONS[TEMPERATURE_ENDPOINT] + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then + attribute = CONFIGURATIONS[ILLUMINANCE_ENDPOINT] + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + if device:supports_capability_by_id(capabilities.tamperAlert.ID) then + attribute = CONFIGURATIONS[TAMPER_ENDPOINT] + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end end +local function device_added(driver, device) + device:emit_event(capabilities.motionSensor.motion.inactive()) + if device:supports_capability_by_id(capabilities.tamperAlert.ID) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function do_refresh(driver, device) + device:send(OccupancySensing.attributes.Occupancy:read(device):to_endpoint(OCCUPANCY_ENDPOINT)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device):to_endpoint(POWER_CONFIGURATION_ENDPOINT)) + + if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(TEMPERATURE_ENDPOINT)) + end + if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then + device:send(IlluminanceMeasurement.attributes.MeasuredValue:read(device):to_endpoint(ILLUMINANCE_ENDPOINT)) + end + if device:supports_capability_by_id(capabilities.tamperAlert.ID) then + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(TAMPER_ENDPOINT)) + end +end + + local function do_configure(driver, device) device:configure() - device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting( - device, - FRIENT_BATTERY_CONFIG.minimum_interval, - FRIENT_BATTERY_CONFIG.maximum_interval, - FRIENT_BATTERY_CONFIG.reportable_change - ):to_endpoint(FRIENT_BATTERY_CONFIG.endpoint)) - device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( - device, - FRIENT_TEMP_CONFIG.minimum_interval, - FRIENT_TEMP_CONFIG.maximum_interval, - FRIENT_TEMP_CONFIG.reportable_change - ):to_endpoint(FRIENT_TEMP_CONFIG.endpoint)) + device:send(device_management.build_bind_request( + device, + zcl_clusters.OccupancySensing.ID, + driver.environment_info.hub_zigbee_eui, + OCCUPANCY_ENDPOINT + )) + + device:send(OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(device, tonumber(DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY)):to_endpoint(OCCUPANCY_ENDPOINT)) + device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(device, tonumber(DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY)):to_endpoint(OCCUPANCY_ENDPOINT)) + device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(device, tonumber(DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD)):to_endpoint(OCCUPANCY_ENDPOINT)) + device:send(OccupancySensing.attributes.Occupancy:configure_reporting(device, 0, 3600):to_endpoint(OCCUPANCY_ENDPOINT)) + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) end -local function added_handler(driver, device) - device:refresh() +local function info_changed(driver, device, event, args) + for name, value in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + if (name == "temperatureSensitivity") then + local input = device.preferences.temperatureSensitivity + local temperatureSensitivity = math.floor(input * 100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, temperatureSensitivity):to_endpoint(TEMPERATURE_ENDPOINT)) + elseif (name == "occupiedToUnoccupiedD") then + local occupiedToUnoccupiedDelay = device.preferences.occupiedToUnoccupiedD or DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY + device:send(OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(device, occupiedToUnoccupiedDelay):to_endpoint(OCCUPANCY_ENDPOINT)) + elseif (name == "unoccupiedToOccupiedD") then + local occupiedToUnoccupiedD = device.preferences.unoccupiedToOccupiedD or DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY + device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(device, occupiedToUnoccupiedD):to_endpoint(OCCUPANCY_ENDPOINT)) + elseif (name == "unoccupiedToOccupiedT") then + local unoccupiedToOccupiedThreshold = device.preferences.unoccupiedToOccupiedT or DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD + device:send(OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(device,unoccupiedToOccupiedThreshold):to_endpoint(OCCUPANCY_ENDPOINT)) + end + end + end end -local frient_driver = { - NAME = "Frient Sensor", +local frient_motion_driver = { + NAME = "frient motion driver", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure, + init = device_init, + infoChanged = info_changed + }, zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, attr = { [OccupancySensing.ID] = { [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler + }, + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler } } }, - lifecycle_handlers = { - init = battery_defaults.build_linear_voltage_init(2.3, 3.0), - added = added_handler, - doConfigure = do_configure + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" - end + can_handle = can_handle_frient_motion_sensor } - -return frient_driver +return frient_motion_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua index 2f326b9c82..5b1a7a80e3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua @@ -90,7 +90,8 @@ local zigbee_motion_driver = { capabilities.relativeHumidityMeasurement, capabilities.battery, capabilities.presenceSensor, - capabilities.contactSensor + capabilities.contactSensor, + capabilities.illuminanceMeasurement }, zigbee_handlers = { attr = { @@ -132,7 +133,6 @@ local zigbee_motion_driver = { }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE } - defaults.register_for_default_handlers(zigbee_motion_driver, zigbee_motion_driver.supported_capabilities, {native_capability_attrs_enabled = true}) local motion = ZigbeeDriver("zigbee-motion", zigbee_motion_driver) 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 new file mode 100644 index 0000000000..3c5b9a5191 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua @@ -0,0 +1,274 @@ +-- Copyright 2025 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 base64 = require "base64" +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local IASZone = zcl_clusters.IASZone +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local OccupancySensing = zcl_clusters.OccupancySensing +local PowerConfiguration = zcl_clusters.PowerConfiguration + +local capabilities = require "st.capabilities" + +local OCCUPANCY_ENDPOINT = 0x22 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 + +local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-motion-battery.yml"), + zigbee_endpoints = { + [0x22] = { + id = 0x22, + manufacturer = "frient A/S", + model = "MOSZB-141", + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500 } + }, + [0x28] = { + id = 0x28, + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x29] = { + id = 0x29, + server_clusters = { 0x0000, 0x0003, 0x0406 } + } + } + } +) + +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_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_coroutine_test( + "Health check should check all relevant attributes", + function() + test.wait_for_events() + test.mock_time.advance_time(50000) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + } + ) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device) + test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") + end + } +) + +test.register_coroutine_test( + "Refresh should read all necessary 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, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT)}) + test.socket.zigbee:__expect_send({ mock_device.id, OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT)}) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + ) + + test.wait_for_events() + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 1 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + OccupancySensing.ID, + OCCUPANCY_ENDPOINT + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:configure_reporting( + mock_device, + 0, + 3600 + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", + function() + local updates = { + preferences = { + occupiedToUnoccupiedD = 200, + unoccupiedToOccupiedD = 1, + unoccupiedToOccupiedT = 2 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, 1):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, 2):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, 200):to_endpoint(OCCUPANCY_ENDPOINT) + }) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..c6cb22dfb1 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua @@ -0,0 +1,440 @@ +-- Copyright 2025 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 base64 = require "base64" +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local IASZone = zcl_clusters.IASZone +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local IlluminanceMeasurement = zcl_clusters.IlluminanceMeasurement +local OccupancySensing = zcl_clusters.OccupancySensing +local PowerConfiguration = zcl_clusters.PowerConfiguration +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement + +local capabilities = require "st.capabilities" + +local IASZONE_ENDPOINT = 0x23 +local ILLUMINANCE_ENDPOINT = 0x27 +local OCCUPANCY_ENDPOINT = 0x22 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local TAMPER_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 + +local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-motion-temp-illuminance-tamper-battery.yml"), + zigbee_endpoints = { + [0x22] = { + id = 0x22, + manufacturer = "frient A/S", + model = "MOSZB-140", + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x000f, 0x0020, 0x0500 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + }, + [0x27] = { + id = 0x27, + server_clusters = { 0x0000, 0x0003, 0x0400 } + }, + [0x28] = { + id = 0x28, + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x29] = { + id = 0x29, + server_clusters = { 0x0000, 0x0003, 0x0406 } + } + } + } +) + +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( + "Motion inactive and tamper clear states when the device is added", function() + test.socket.matter:__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.motionSensor.motion.inactive()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: tamper detected", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 1 + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0004, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: tamper clear", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 0 + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } +) + +test.register_message_test( + "Illuminance report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 21370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + } + } +) + +test.register_coroutine_test( + "Health check should check all relevant attributes", + function() + test.wait_for_events() + test.mock_time.advance_time(50000) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(TAMPER_ENDPOINT) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + } + ) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device) + test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") + end + } +) + +test.register_coroutine_test( + "Refresh should read all necessary 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, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(TAMPER_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT)}) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + + test.wait_for_events() + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 1 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + OccupancySensing.ID, + OCCUPANCY_ENDPOINT + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:configure_reporting( + mock_device, + 0, + 3600 + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + 10 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IlluminanceMeasurement.ID, + ILLUMINANCE_ENDPOINT + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 10, + 3600, + 0x2711 + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Temperature Sensitivity, Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + occupiedToUnoccupiedD = 200, + unoccupiedToOccupiedD = 1, + unoccupiedToOccupiedT = 2 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, 1):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, 2):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, 200):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_sensor.lua deleted file mode 100644 index c2eb704ccc..0000000000 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_sensor.lua +++ /dev/null @@ -1,175 +0,0 @@ --- Copyright 2022 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 IASZone = clusters.IASZone -local PowerConfiguration = clusters.PowerConfiguration -local TemperatureMeasurement = clusters.TemperatureMeasurement -local OccupancyAttribute = clusters.OccupancySensing.attributes.Occupancy -local capabilities = require "st.capabilities" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" -local t_utils = require "integration_test.utils" - -local mock_device = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("motion-temp-battery.yml"), - fingerprinted_endpoint_id = 0x01, - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "frient A/S", - model = "MOSZB-140", - server_clusters = {0x0001, 0x0020, 0x0402, 0x0500} - } - } - } -) - -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( - "Configure should configure all necessary attributes", - function() - test.wait_for_events() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.zigbee:__expect_send({ - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 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, - TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 30, 600, 100) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, TemperatureMeasurement.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 300, 0) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, IASZone.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 30, 300, 100):to_endpoint(0x26) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1):to_endpoint(0x23) - }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Added should refresh all necessary attributes", - function() - test.wait_for_events() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.zigbee:__expect_send({ - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.ZoneStatus:read(mock_device) - }) - end -) - -test.register_coroutine_test( - "Battery Voltage test cases", - function() - local battery_test_map = { - ["frient A/S"] = { - [32] = 100, - [31] = 100, - [29] = 86, - [26] = 43, - [24] = 14, - [23] = 0, - [15] = 0 - } - } - - for voltage, batt_perc in pairs(battery_test_map[mock_device:get_manufacturer()]) do - test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc)) ) - test.wait_for_events() - end - end -) - -test.register_message_test( - "Reported motion should be handled: active", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, OccupancyAttribute:build_test_attr_report(mock_device, 0x01) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) - } - } -) - -test.register_message_test( - "Reported motion should be handled: inactive", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, OccupancyAttribute:build_test_attr_report(mock_device, 0x00) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) - } - } -) - -test.run_registered_tests() From f9fa83c3d51450deddf9227fb461865085793682 Mon Sep 17 00:00:00 2001 From: Huangxiangjie Date: Wed, 11 Jun 2025 22:27:07 +0800 Subject: [PATCH 019/449] Add air purifier profile (#2165) * Add air purifier profile --------- Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --- ...ier-hepa-wind-aqs-pm25-meas-pm25-level.yml | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml diff --git a/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml new file mode 100644 index 0000000000..3f014f0b91 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/profiles/air-purifier-hepa-wind-aqs-pm25-meas-pm25-level.yml @@ -0,0 +1,32 @@ +name: air-purifier-hepa-wind-aqs-pm25-meas-pm25-level +components: +- id: main + label: Main + capabilities: + - id: airPurifierFanMode + version: 1 + - id: fanSpeedPercent + version: 1 + - id: windMode + version: 1 + - id: airQualityHealthConcern + version: 1 + - id: fineDustSensor + version: 1 + - id: fineDustHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirPurifier +- id: hepaFilter + label: Hepa Filter + capabilities: + - id: filterState + version: 1 + - id: filterStatus + version: 1 + categories: + - name: AirPurifier \ No newline at end of file From 9cf1694bd04c6e4352aa1cda7b8dcbd4edb0d85f Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:07:44 -0500 Subject: [PATCH 020/449] Matter Sensor: Add motion-illuminance-temperature-humidity-battery profile and fingerprint (#2191) * add new sensor fingerprint and profiles (battery/batteryLevel versions) --- .../matter-sensor/fingerprints.yml | 8 ++++++ ...luminance-temperature-humidity-battery.yml | 25 +++++++++++++++++++ ...ance-temperature-humidity-batteryLevel.yml | 25 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml create mode 100644 drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 65cd70826e..a5ab7a5e2e 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -205,6 +205,14 @@ matterGeneric: - id: 0x0106 # Light Sensor - id: 0x0302 # Temperature Sensor deviceProfileName: motion-illuminance-temperature-battery + - id: "matter/motion-illum-temp-humidity" + deviceLabel: Matter Motion/Illuminance Sensor + deviceTypes: + - id: 0x0107 # Occupancy Sensor + - id: 0x0106 # Light Sensor + - id: 0x0302 # Temperature Sensor + - id: 0x0307 # Humidity Sensor + deviceProfileName: motion-illuminance-temperature-humidity-battery - id: "matter/motion-sensor" deviceLabel: Matter Motion Sensor deviceTypes: diff --git a/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml new file mode 100644 index 0000000000..689f0c1eac --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-battery.yml @@ -0,0 +1,25 @@ +name: motion-illuminance-temperature-humidity-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true diff --git a/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml new file mode 100644 index 0000000000..00708e6430 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/motion-illuminance-temperature-humidity-batteryLevel.yml @@ -0,0 +1,25 @@ +name: motion-illuminance-temperature-humidity-batteryLevel +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true From 534721b9ae6635419ecff71196ee2478db134bee Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Mon, 16 Jun 2025 13:34:01 -0500 Subject: [PATCH 021/449] philips-hue: Fix batched command handling for grandchildren devices --- .../philips-hue/src/utils/batched_command_utils.lua | 6 +++--- drivers/SmartThings/philips-hue/src/utils/init.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/utils/batched_command_utils.lua b/drivers/SmartThings/philips-hue/src/utils/batched_command_utils.lua index bf2d615717..241ca95f13 100644 --- a/drivers/SmartThings/philips-hue/src/utils/batched_command_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/batched_command_utils.lua @@ -114,20 +114,20 @@ function batched_command_utils.sort_batch(driver, batch) for _, to_inspect in ipairs(batch) do -- Check if we can handle this in a batch if not batched_command_utils.get_handler(to_inspect) then - misfits.insert(to_inspect) + table.insert(misfits, to_inspect) goto continue end -- First key off bridge. local device = driver:get_device_info(to_inspect.device_uuid) if not device then - misfits.insert(to_inspect) + table.insert(misfits, to_inspect) goto continue end local parent_id = device.parent_device_id or device:get_field(Fields.PARENT_DEVICE_ID) local bridge_device = utils.get_hue_bridge_for_device(driver, device, parent_id, true) if not bridge_device then - misfits.insert(to_inspect) + table.insert(misfits, to_inspect) goto continue end sorted_batch[bridge_device] = sorted_batch[bridge_device] or KVCounter() diff --git a/drivers/SmartThings/philips-hue/src/utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua index 4c5a29dc54..0819286ab5 100644 --- a/drivers/SmartThings/philips-hue/src/utils/init.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -368,7 +368,7 @@ function utils.get_hue_bridge_for_device(driver, device, parent_device_id, quiet return parent_device --[[ @as HueBridgeDevice ]] end - return utils.get_hue_bridge_for_device(driver, parent_device, quiet) + return utils.get_hue_bridge_for_device(driver, parent_device, nil, quiet) end --- build a exponential backoff time value generator From c9e01fc90d4e708d5ea72572961c46a9959f493c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 17 Jun 2025 08:00:41 -0700 Subject: [PATCH 022/449] Merge pull request #2186 from SmartThingsCommunity/new_device/WWSTCERT-6440 WWSTCERT-6440 Smart Mechanical Keyboard MK1 --- 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 601c4dda6f..8806ee6c5c 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -148,6 +148,11 @@ matterManufacturer: vendorId: 0x1407 productId: 0x1288 deviceProfileName: matter-bridge + - id: "5127/5000" + deviceLabel: Smart Mechanical Keyboard MK1 + vendorId: 0x1407 + productId: 0x1388 + deviceProfileName: 12-button-keyboard #ELEGRP - id: "5158/3" From 6bdecfef32fb631508f9a20f3bae8200e9850f1c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 17 Jun 2025 10:22:33 -0700 Subject: [PATCH 023/449] Merge pull request #2201 from SmartThingsCommunity/new_device/WWSTCERT-6475 WWSTCERT-6475 Cync Full Color RS Can --- 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 8806ee6c5c..1521fa8166 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -399,6 +399,11 @@ matterManufacturer: vendorId: 0x1339 productId: 0x0075 deviceProfileName: switch-level + - id: "4921/22" + deviceLabel: Cync Full Color RS Can + vendorId: 0x1339 + productId: 0x0016 + deviceProfileName: light-color-level-2000K-7000K #Ledvance - id: "4489/843" deviceLabel: Matter Filament RGBW From 6060dc15b9103e5058a6ac7921fdc1d41bb5b6ac Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Tue, 17 Jun 2025 15:06:43 +0900 Subject: [PATCH 024/449] Update Schedule feature on new-matter-lock driver (#2144) Signed-off-by: Hunsup Jung --- .../profiles/lock-user-schedule-unlatch.yml | 12 + .../profiles/lock-user-unlatch.yml | 3 - .../matter-lock/src/lock_utils.lua | 4 +- .../matter-lock/src/new-matter-lock/init.lua | 355 +++++++++++++++++- .../src/test/test_new_matter_lock.lua | 28 +- 5 files changed, 391 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml index 1e03f51b92..240f31c230 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-schedule-unlatch.yml @@ -71,6 +71,12 @@ deviceConfig: displayType: pushButton pushButton: command: unlatch + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 automation: conditions: - component: main @@ -88,6 +94,12 @@ deviceConfig: value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}' - key: not fully locked value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}' + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 actions: - component: main capability: lock diff --git a/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml index d1846f1c2e..65322094e2 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-user-unlatch.yml @@ -75,9 +75,6 @@ deviceConfig: - component: main capability: remoteControlStatus version: 1 - - component: main - capability: batteryLevel - version: 1 automation: conditions: - component: main diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index d9c43c30d2..5d92c55afa 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -38,7 +38,9 @@ local lock_utils = { SCHEDULE_START_HOUR = "scheduleStartHour", SCHEDULE_START_MINUTE = "scheduleStartMinute", SCHEDULE_END_HOUR = "scheduleEndHour", - SCHEDULE_END_MINUTE = "scheduleEndMinute" + SCHEDULE_END_MINUTE = "scheduleEndMinute", + SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 3e41d57296..e3dba5f458 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -26,14 +26,19 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource + local INITIAL_COTA_INDEX = 1 local ALL_INDEX = 0xFFFE +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 NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 {0x147F, 0x0001}, -- U-tec {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 + {0x101D, 0x8110}, -- Yale, New Lock {0x1533, 0x0001}, -- eufy, E31 {0x1533, 0x0002}, -- eufy, E30 {0x1533, 0x0003}, -- eufy, C34 @@ -265,6 +270,7 @@ local function operating_modes_handler(driver, device, ib, response) device:emit_event(capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) end end + ------------------------------------- -- Number Of Total Users Supported -- ------------------------------------- @@ -701,7 +707,7 @@ local function add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule device:emit_event(capabilities.lockSchedules.weekDaySchedules(week_schedule_table, {visibility = {displayed = false}})) end -local function delete_week_schedule_to_table(device, userIdx, scheduleIdx) +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", @@ -741,6 +747,154 @@ local function delete_week_schedule_to_table(device, userIdx, scheduleIdx) 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 + -------------- -- Add User -- -------------- @@ -1049,6 +1203,8 @@ local function clear_user_response_handler(driver, device, ib, response) if status == "success" then delete_user_from_table(device, userIdx) delete_credential_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 @@ -1206,6 +1362,7 @@ local function set_credential_response_handler(driver, device, ib, response) 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 status = "success" local elements = ib.info_block.data.elements @@ -1218,7 +1375,6 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - local userType = device:get_field(lock_utils.USER_TYPE) add_user_to_table(device, elements.user_index.value, userType) end @@ -1243,7 +1399,32 @@ local function set_credential_response_handler(driver, device, ib, response) } ) device:emit_event(event) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + + -- 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) 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 + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end return end @@ -1283,7 +1464,9 @@ local function set_credential_response_handler(driver, device, ib, response) 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 userType == "guest" then + 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 @@ -1611,7 +1794,7 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) -- Delete Week Day Schedule to table if status == "success" then - delete_week_schedule_to_table(device, userIdx, scheduleIdx) + delete_week_schedule_from_table(device, userIdx, scheduleIdx) else device.log.warn(string.format("Failed to clear week day schedule: %s", status)) end @@ -1634,16 +1817,177 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) 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 + local busy = check_busy_state(device) + if busy == true then + local result = { + commandName = cmdName, + statusCode = "busy" + } + local event = capabilities.lockSchedules.commandResult( + result, + { + state_change = true, + visibility = {displayed = false} + } + ) + device:emit_event(event) + 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 + 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 result = { + commandName = cmdName, + userIndex = userIdx, + scheduleIndex = scheduleIdx, + statusCode = status + } + local event = capabilities.lockSchedules.commandResult( + result, + { + state_change = true, + visibility = {displayed = false} + } + ) + device:emit_event(event) + 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 + local busy = check_busy_state(device) + if busy == true then + local result = { + commandName = cmdName, + statusCode = "busy" + } + local event = capabilities.lockSchedules.commandResult( + result, + { + state_change = true, + visibility = {displayed = false} + } + ) + device:emit_event(event) + 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 ---------------- @@ -1773,6 +2117,7 @@ local new_matter_lock_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, }, }, }, 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 347b91a5c7..9659c9484d 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 @@ -1155,6 +1155,18 @@ test.register_coroutine_test( capabilities.lockCredentials.credentials({}, {visibility={displayed=false}}) ) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1230,6 +1242,18 @@ test.register_coroutine_test( capabilities.lockCredentials.credentials({}, {visibility={displayed=false}}) ) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1460,11 +1484,11 @@ test.register_coroutine_test( "654123", -- credential_data 1, -- user_index nil, -- user_status - DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + nil -- user_type ), } ) - test.wait_for_events() + -- test.wait_for_events() end ) From e7f7ffb1bd6100344db3d03eb39df0ec2494ca73 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 20 Jun 2025 10:44:01 -0700 Subject: [PATCH 025/449] Merge pull request #2207 from campenz/feature/HCS-6011 HCS-6011: add v4-hub profile --- drivers/SmartThings/hub/fingerprints.yml | 6 ++- drivers/SmartThings/hub/profiles/v4-hub.yml | 41 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/hub/profiles/v4-hub.yml diff --git a/drivers/SmartThings/hub/fingerprints.yml b/drivers/SmartThings/hub/fingerprints.yml index 96c79841a5..317df13f8b 100644 --- a/drivers/SmartThings/hub/fingerprints.yml +++ b/drivers/SmartThings/hub/fingerprints.yml @@ -18,7 +18,11 @@ hub: - id: "cooktop-hub" deviceLabel: SmartThings Hub hardwareType: SAMSUNG_COOKTOP_TIZEN_OPEN - deviceProfileName: cooktop-hub + deviceProfileName: cooktop-hub + - id: "v4-hub" + deviceLabel: SmartThings Hub + hardwareType: V4_HUB + deviceProfileName: v4-hub hubThing: - id: "hub-thing" deviceLabel: SmartThings Hub diff --git a/drivers/SmartThings/hub/profiles/v4-hub.yml b/drivers/SmartThings/hub/profiles/v4-hub.yml new file mode 100644 index 0000000000..f7c85625b6 --- /dev/null +++ b/drivers/SmartThings/hub/profiles/v4-hub.yml @@ -0,0 +1,41 @@ +name: v4-hub +components: + - id: main + label: main + capabilities: + - id: bridge + version: 1 + ephemeral: false + - id: wifiInformation + version: 1 + - id: sec.diagnosticsInformation + version: 1 + - id: sec.networkConfiguration + version: 1 + - id: firmwareUpdate + version: 1 + - id: samsungim.devicestatus + version: 1 + - id: sec.wifiConfiguration + version: 1 + categories: + - name: Hub + categoryType: manufacturer +deviceConfig: + dashboard: + states: + - component: main + capability: bridge + version: 1 + dpInfo: + - os: ios + dpUri: 'plugin://com.samsung.ios.plugin.stplugin/assets/files/index.html' + - os: android + dpUri: 'plugin://com.samsung.android.plugin.stplugin' + - os: web + dpUri: 'wwst://com.samsung.one.plugin.stplugin' + arguments: + - key: int_hub + value: 'wwst://com.samsung.one.plugin.chargerplugin' +metadata: + ocfDeviceType: x.com.st.d.hub From 6328479449094ea6b97ef547d1468f669d90607b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 20 Jun 2025 12:09:53 -0700 Subject: [PATCH 026/449] Merge pull request #2203 from SmartThingsCommunity/new_device/WWSTCERT-6539 WWSTCERT-6539 Shelly 1PM Gen3 --- 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 1521fa8166..54d112e461 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -730,6 +730,11 @@ matterManufacturer: vendorId: 0x1490 productId: 0x1853 deviceProfileName: plug-binary + - id: "5264/4121" + deviceLabel: Shelly 1PM Gen3 + vendorId: 0x1490 + productId: 0x1019 + deviceProfileName: plug-binary #SONOFF - id: "SONOFF MINIR4M" deviceLabel: Smart Plug-in Unit From d0d0259f998f8ca3288ae93f52d651ad60a8a695 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 23 Jun 2025 15:37:14 -0700 Subject: [PATCH 027/449] fix bad merge --- drivers/SmartThings/matter-rvc/src/init.lua | 3 --- drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua | 4 ---- 2 files changed, 7 deletions(-) diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 25cbefe019..c8fae5aaad 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -15,9 +15,6 @@ local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local area_type = require "Global.types.AreaTypeTag" -local landmark = require "Global.types.LandmarkTag" -local embedded_cluster_utils = require "embedded_cluster_utils" local area_type = require "Global.types.AreaTypeTag" local landmark = require "Global.types.LandmarkTag" 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 b4944df4d8..1fb2afb752 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -1284,7 +1284,6 @@ local locationDescriptorStruct = require "Global.types.LocationDescriptorStruct" local areaInfoStruct = require "ServiceArea.types.AreaInfoStruct" local areaStruct = require "ServiceArea.types.AreaStruct" local landmarkInfoStruct = require "ServiceArea.types.LandmarkInfoStruct" - test.register_message_test( "Supported ServiceAreas must be registered", { @@ -1315,7 +1314,6 @@ test.register_message_test( ) local selectAreasStatus = require "ServiceArea.types.SelectAreasStatus" - test.register_message_test( "ServiceArea attribute report must emit appropriate capability event", { @@ -1339,7 +1337,6 @@ test.register_message_test( ) local uint32_dt = require "st.matter.data_types.Uint32" - test.register_message_test( "Select ServiceAreas command must trigger appropriate matter cluster", { @@ -1362,7 +1359,6 @@ test.register_message_test( } ) - test.register_message_test( "Selected ServiceAreasResponse must log Success status or emit last valid selectedAreas capability on non-Success status", { From b128a14d339423e8f4dabe91123a44c2284c97f3 Mon Sep 17 00:00:00 2001 From: Douglas Stephen Date: Tue, 24 Jun 2025 16:20:51 -0500 Subject: [PATCH 028/449] Merge pull request #2213 from SmartThingsCommunity/fix/various-sonos-crashes --- .../sonos/src/api/sonos_connection.lua | 10 +- .../sonos/src/api/sonos_ssdp_discovery.lua | 7 + .../sonos/src/lifecycle_handlers.lua | 194 +++++++++--------- .../SmartThings/sonos/src/sonos_driver.lua | 58 ++++-- drivers/SmartThings/sonos/src/sonos_state.lua | 13 +- drivers/SmartThings/sonos/src/ssdp.lua | 15 +- drivers/SmartThings/sonos/src/utils.lua | 50 ++++- 7 files changed, 227 insertions(+), 120 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 4fdfbbd093..1ae8465915 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -239,7 +239,9 @@ end ---@param sonos_conn SonosConnection local function _oauth_reconnect_task(sonos_conn) log.debug("Spawning reconnect task for ", sonos_conn.device.label) - if not sonos_conn.driver:is_waiting_for_oauth_token() then + local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + local unauthorized = (check_auth == false) + if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then sonos_conn.driver:request_oauth_token() end local token_receive_handle, err = sonos_conn.driver:oauth_token_event_subscribe() @@ -264,7 +266,11 @@ local function _oauth_reconnect_task(sonos_conn) return end end - if not sonos_conn.driver:is_waiting_for_oauth_token() then + + check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + unauthorized = (check_auth == false) + + if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then sonos_conn.driver:request_oauth_token() end cosock.socket.sleep(backoff()) diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index 32a63a012c..88e0cddbd9 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -298,6 +298,13 @@ function sonos_ssdp.spawn_persistent_ssdp_task() end if info_to_send then + if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then + log.error_with( + { hub_logs = true }, + st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") + ) + return + end event_bus:send(info_to_send) local mac_addr = utils.extract_mac_addr(info_to_send.discovery_info.device) local waiting_handles = task_handle.waiting_for_unique_key[unique_key] or {} diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index ddecd41a1d..747381f798 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -59,119 +59,127 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local mac_addr = device.device_network_id local player_info_tx, player_info_rx = cosock.channel.new() while true do - driver.ssdp_task:get_player_info(player_info_tx, mac_addr) - local recv_ready, _, select_err = cosock.socket.select({ player_info_rx }, nil, nil) - - if type(recv_ready) == "table" and recv_ready[1] == player_info_rx then - local info, recv_err = player_info_rx:receive() - if not info then - device.log.warn(string.format("error receiving device info: %s", recv_err)) - else - ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } - local auth_success, api_key_or_err = driver:check_auth(info) - if not auth_success then - device:offline() - if auth_success == false and api_version >= 14 then - local token_event_receive = driver:oauth_token_event_subscribe() - if not token_event_receive then - log.error("token event bus closed, aborting initialization") - return - end - token_event_receive:settimeout(30) - local token, token_recv_err - -- max 30 mins - local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn(string.format("Error sending token request: %s", request_token_err)) + if driver.ssdp_task then + driver.ssdp_task:get_player_info(player_info_tx, mac_addr) + local recv_ready, _, select_err = cosock.socket.select({ player_info_rx }, nil, nil) + + if type(recv_ready) == "table" and recv_ready[1] == player_info_rx then + local info, recv_err = player_info_rx:receive() + if not info then + device.log.warn(string.format("error receiving device info: %s", recv_err)) + else + ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } + local auth_success, api_key_or_err = driver:check_auth(info) + if not auth_success then + device:offline() + if auth_success == false and api_version >= 14 then + local token_event_receive = driver:oauth_token_event_subscribe() + if not token_event_receive then + log.error("token event bus closed, aborting initialization") + return end - end - - local backoff_timer = nil - while not token do - local send_request = false - -- we use the backoff to create a timer and utilize a select loop here, instead of - -- utilizing a sleep, so that we can create a long delay on our polling of the cloud - -- without putting ourselves in a situation where we're sleeping for an extended period - -- of time so that we don't sleep through the users's log-in attempt and fail to resume - -- our connection attempts in a timely manner. - -- - -- The backoff caps at 30 mins, as commented above - if not backoff_timer then - backoff_timer = cosock.timer.create_oneshot(backoff_builder()) + token_event_receive:settimeout(30) + local token, token_recv_err + -- max 30 mins + local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) + if not driver:is_waiting_for_oauth_token() then + local _, request_token_err = driver:request_oauth_token() + if request_token_err then + log.warn(string.format("Error sending token request: %s", request_token_err)) + end end - local token_recv_ready, _, token_select_err = - cosock.socket.select({ token_event_receive, backoff_timer }, nil, nil) - if token_select_err then - log.warn(string.format("select error: %s", token_select_err)) - end + local backoff_timer = nil + while not token do + local send_request = false + -- we use the backoff to create a timer and utilize a select loop here, instead of + -- utilizing a sleep, so that we can create a long delay on our polling of the cloud + -- without putting ourselves in a situation where we're sleeping for an extended period + -- of time so that we don't sleep through the users's log-in attempt and fail to resume + -- our connection attempts in a timely manner. + -- + -- The backoff caps at 30 mins, as commented above + if not backoff_timer then + backoff_timer = cosock.timer.create_oneshot(backoff_builder()) + end + local token_recv_ready, _, token_select_err = + cosock.socket.select({ token_event_receive, backoff_timer }, nil, nil) - token, token_recv_err = nil, nil - for _, receiver in pairs(token_recv_ready or {}) do - if receiver == backoff_timer then - -- we just make a note that the backoff has elapsed, rather than - -- put a request in flight immediately. - -- - -- This is just in case both receivers are ready, so that we can prioritize - -- handling the token instead of putting another request in flight. - send_request = true - backoff_timer:handled() - backoff_timer = nil + if token_select_err then + log.warn(string.format("select error: %s", token_select_err)) end - if receiver == token_event_receive then - token, token_recv_err = token_event_receive:receive() + token, token_recv_err = nil, nil + for _, receiver in pairs(token_recv_ready or {}) do + if receiver == backoff_timer then + -- we just make a note that the backoff has elapsed, rather than + -- put a request in flight immediately. + -- + -- This is just in case both receivers are ready, so that we can prioritize + -- handling the token instead of putting another request in flight. + send_request = true + backoff_timer:handled() + backoff_timer = nil + end + + if receiver == token_event_receive then + token, token_recv_err = token_event_receive:receive() + end end - end - if token_recv_err == "timeout" then - log.debug("timeout waiting for OAuth token in reconnect task") - elseif token_recv_err and not token then - log.warn( - string.format( - "Unexpected error on token event receive bus: %s", - token_recv_err + if token_recv_err == "timeout" then + log.debug("timeout waiting for OAuth token in reconnect task") + elseif token_recv_err and not token then + log.warn( + string.format( + "Unexpected error on token event receive bus: %s", + token_recv_err + ) ) - ) - end + end - if send_request then - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn( - string.format("Error sending token request: %s", request_token_err) - ) + if send_request then + if not driver:is_waiting_for_oauth_token() then + local _, request_token_err = driver:request_oauth_token() + if request_token_err then + log.warn( + string.format("Error sending token request: %s", request_token_err) + ) + end end end end + else + device.log.error( + string.format( + "error while checking authentication: %s, marking device offline", + api_key_or_err + ) + ) end else - device.log.error( - string.format( - "error while checking authentication: %s, marking device offline", - api_key_or_err - ) + local success, error, error_code = + driver:handle_player_discovery_info(api_key_or_err, info, device) + if success then + return + end + log.error_with( + { hub_logs = true }, + "Error handling Sonos player initialization: %s, error code: %s", + error, + (error_code or "N/A") ) end - else - local success, error, error_code = - driver:handle_player_discovery_info(api_key_or_err, info, device) - if success then - return - end - log.error( - "Error handling Sonos player initialization: %s, error code: %s", - error, - (error_code or "N/A") - ) end + else + device.log.warn( + string.format("select error waiting for initialization device info: %s", select_err) + ) end else - device.log.warn( - string.format("select error waiting for initialization device info: %s", select_err) + device.log.error_with( + { hub_logs = true }, + string.format("Driver wasn't able to spin up SSDP task, cannot initialize devices.") ) end end diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 2bd4e7724d..36614b0915 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -4,7 +4,6 @@ local cosock = require "cosock" local json = require "st.json" local log = require "log" local net_url = require "net.url" -local security = require "st.security" local st_utils = require "st.utils" local sonos_ssdp = require "api.sonos_ssdp_discovery" @@ -17,6 +16,15 @@ local SonosDisco = require "disco" local SonosDriverLifecycleHandlers = require "lifecycle_handlers" local SonosState = require "sonos_state" +local security_load_success, security = pcall(require, "st.security") +if not security_load_success then + log.warn_with( + { hub_logs = true }, + string.format("Unable to load `st.security` module: %s", security) + ) + security = nil +end + local ONE_HOUR_IN_SECONDS = 3600 ---@class SonosDriver: Driver @@ -94,13 +102,13 @@ end ---@return boolean function SonosDriver:is_waiting_for_oauth_token() - return (api_version >= 14) and self.waiting_for_oauth_token + return (api_version >= 14 and security ~= nil) and self.waiting_for_oauth_token end ---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed ---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success function SonosDriver:oauth_token_event_subscribe() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end return self.oauth_token_bus:subscribe() @@ -199,7 +207,7 @@ function SonosDriver:handle_startup_state_received() end function SonosDriver:get_fallback_api_key() - if api_version < 14 then + if api_version < 14 or security == nil then return SonosApi.api_keys.s1_key end @@ -218,7 +226,7 @@ end function SonosDriver:check_auth(info_or_device) local maybe_token, _ = self:get_oauth_token() - local token_valid = (api_version >= 14) + local token_valid = (api_version >= 14 and security ~= nil) and self.oauth and self.oauth.endpoint_app_info and self.oauth.endpoint_app_info.state == "connected" @@ -280,30 +288,44 @@ function SonosDriver:check_auth(info_or_device) return true, api_key end end + + local unauthorized = false for _, api_key in pairs(SonosApi.api_keys) do local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) + + if response._objectType == "globalError" then + unauthorized = (response.errorCode == "ERROR_NOT_AUTHORIZED") + if not unauthorized then + return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) + end + end + if not response or response_err then return nil, string.format("Error while making REST API call: %s", (response_err or "")) end - if response._objectType == "globalError" and response.errorCode ~= "ERROR_NOT_AUTHORIZED" then - return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) - end - if response._objectType == "groups" then return true, api_key end end - return false + if unauthorized then + return false + end + + return nil, + string.format( + "Unable to determine Authentication Status for %s", + st_utils.stringify_table(info_or_device) + ) end ---@return any? ret nil on permissions violation ---@return string? error nil on success function SonosDriver:request_oauth_token() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end local maybe_token, maybe_err = self:get_oauth_token() @@ -324,7 +346,7 @@ end ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ---@return "token expired"|"no token"|"not supported"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end self.hub_augmented_driver_data = self.hub_augmented_driver_data or {} @@ -389,6 +411,8 @@ local function make_ssdp_event_handler( log.warn(string.format("Error on token event bus receive: %s", receive_err)) else for _, event in pairs(unauthorized) do + -- shouldn't need a nil check on the ssdp_task here since this whole function + -- won't get called unless the task is successfully spawned. driver.ssdp_task:publish(event) end unauthorized = {} @@ -410,7 +434,10 @@ local function make_ssdp_event_handler( if err_code == "ERROR_NOT_AUTHORIZED" then unauthorized[unique_key] = event end - log.warn(string.format("Failed to handle discovered speaker: %s", handle_err)) + log.warn_with( + { hub_logs = true }, + string.format("Failed to handle discovered speaker: %s", handle_err) + ) else discovered[unique_key] = true end @@ -431,7 +458,7 @@ end function SonosDriver:start_ssdp_event_task() local ssdp_task, err = sonos_ssdp.spawn_persistent_ssdp_task() if err then - log.error(string.format("Unable to create SSDP task: %s", err)) + log.error_with({ hub_logs = true }, string.format("Unable to create SSDP task: %s", err)) end if ssdp_task then self.ssdp_task = ssdp_task @@ -535,6 +562,9 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end if not device_mac_addr then + if not (info and info.discovery_info and info.discovery_info.device) then + return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") + end device_mac_addr = utils.extract_mac_addr(info.discovery_info.device) end diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index e2b4cbd7ed..d9b6b7f0b4 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -183,8 +183,17 @@ end function SonosState:update_device_record_group_info(household, group, device) local player_id = device:get_field(PlayerFields.PLAYER_ID) local group_role - if (player_id and group and group.id and group.coordinatorId) and player_id == group.coordinatorId then - local player_ids_list = household.groups[group.id].playerIds or {} + if + ( + type(household) == "table" + and type(household.groups) == "table" + and player_id + and group + and group.id + and group.coordinatorId + ) and player_id == group.coordinatorId + then + local player_ids_list = (household.groups[group.id] or {}).playerIds or {} if #player_ids_list > 1 then group_role = "primary" else diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua index c96be3c387..7e3b06a198 100644 --- a/drivers/SmartThings/sonos/src/ssdp.lua +++ b/drivers/SmartThings/sonos/src/ssdp.lua @@ -294,7 +294,8 @@ function _ssdp_mt:next_msearch_response() if possible_locations[recv_ip_or_err] == nil then return Err( string.format( - "IP addres [%s] from socket receivefrom doesn't match any of the reply locations: %s", + "IP address [%s] from socket receivefrom doesn't match any of the reply locations: %s", + recv_ip_or_err, table.concat(location_candidates, ", ") ) ) @@ -425,8 +426,11 @@ end function Ssdp.new_search_instance(mx) local udp_sock, sock_err = socket.udp() if sock_err or not udp_sock then - log.error(string.format("Error opening UDP socket for SSDP search: %s", sock_err)) - return nil, "sock_err" + log.error_with( + { hub_logs = true }, + string.format("Error opening UDP socket for SSDP search: %s", sock_err) + ) + return nil, sock_err end local listen_ip = "0.0.0.0" @@ -435,7 +439,10 @@ function Ssdp.new_search_instance(mx) local _, bind_err = udp_sock:setsockname(listen_ip, listen_port) if bind_err then - log.error(string.format("Unable to bind UDP socket for SSDP search: %s", bind_err)) + log.error_with( + { hub_logs = true }, + string.format("Unable to bind UDP socket for SSDP search: %s", bind_err) + ) return nil, bind_err end diff --git a/drivers/SmartThings/sonos/src/utils.lua b/drivers/SmartThings/sonos/src/utils.lua index b88daad6ca..5ffb3a8a9d 100644 --- a/drivers/SmartThings/sonos/src/utils.lua +++ b/drivers/SmartThings/sonos/src/utils.lua @@ -1,4 +1,5 @@ local log = require "log" +local st_utils = require "st.utils" local PlayerFields = require "fields".SonosPlayerFields @@ -125,14 +126,47 @@ end ---@param tbl table ---@param key string local function __case_insensitive_key_index(tbl, key) - assert(type(key) == "string", "key for CaseInsensitiveKeyTable must be a string!") - local lowercase = key:lower() - return rawget(tbl, lowercase) + if type(key) ~= "string" then + local fmt_val + if type(key) == "table" then + fmt_val = st_utils.stringify_table(key) + else + fmt_val = key or "" + end + log.warn_with( + { hub_logs = true }, + string.format( + "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", + fmt_val, + type(key) + ) + ) + return nil + else + local lowercase = key:lower() + return rawget(tbl, lowercase) + end end local function __case_insensitive_key_newindex(tbl, key, value) - assert(type(key) == "string", "key for CaseInsensitiveKeyTable must be a string!") - rawset(tbl, key:lower(), value) + if type(key) ~= "string" then + local fmt_val + if type(key) == "table" then + fmt_val = st_utils.stringify_table(key) + else + fmt_val = key or "" + end + log.warn_with( + { hub_logs = true }, + string.format( + "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", + fmt_val, + type(key) + ) + ) + else + rawset(tbl, key:lower(), value) + end end local _case_insensitive_key_mt = { @@ -147,6 +181,12 @@ end ---@param sonos_device_info SonosDeviceInfo function utils.extract_mac_addr(sonos_device_info) + if type(sonos_device_info) ~= "table" or type(sonos_device_info.serialNumber) ~= "string" then + log.error_with( + { hub_logs = true }, + string.format("Bad sonos device info passed to `extract_mac_addr`: %s", sonos_device_info) + ) + end local mac, _ = sonos_device_info.serialNumber:match("(.*):.*"):gsub("-", "") return utils.normalize_mac_address(mac) end From 2a2709e4f4bc58fa87482dbc7c2b38e9c5cee365 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Wed, 25 Jun 2025 12:26:52 -0500 Subject: [PATCH 029/449] Merge pull request #2217 from SmartThingsCommunity/beta-hotfix/sonos-crashes Hotfix Sonos Crashes --- .../sonos/src/api/sonos_connection.lua | 10 +- .../sonos/src/api/sonos_ssdp_discovery.lua | 7 + .../sonos/src/lifecycle_handlers.lua | 194 +++++++++--------- .../SmartThings/sonos/src/sonos_driver.lua | 58 ++++-- drivers/SmartThings/sonos/src/sonos_state.lua | 13 +- drivers/SmartThings/sonos/src/ssdp.lua | 15 +- drivers/SmartThings/sonos/src/utils.lua | 50 ++++- 7 files changed, 227 insertions(+), 120 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 4fdfbbd093..1ae8465915 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -239,7 +239,9 @@ end ---@param sonos_conn SonosConnection local function _oauth_reconnect_task(sonos_conn) log.debug("Spawning reconnect task for ", sonos_conn.device.label) - if not sonos_conn.driver:is_waiting_for_oauth_token() then + local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + local unauthorized = (check_auth == false) + if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then sonos_conn.driver:request_oauth_token() end local token_receive_handle, err = sonos_conn.driver:oauth_token_event_subscribe() @@ -264,7 +266,11 @@ local function _oauth_reconnect_task(sonos_conn) return end end - if not sonos_conn.driver:is_waiting_for_oauth_token() then + + check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + unauthorized = (check_auth == false) + + if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then sonos_conn.driver:request_oauth_token() end cosock.socket.sleep(backoff()) diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index 32a63a012c..88e0cddbd9 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -298,6 +298,13 @@ function sonos_ssdp.spawn_persistent_ssdp_task() end if info_to_send then + if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then + log.error_with( + { hub_logs = true }, + st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") + ) + return + end event_bus:send(info_to_send) local mac_addr = utils.extract_mac_addr(info_to_send.discovery_info.device) local waiting_handles = task_handle.waiting_for_unique_key[unique_key] or {} diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index ddecd41a1d..747381f798 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -59,119 +59,127 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local mac_addr = device.device_network_id local player_info_tx, player_info_rx = cosock.channel.new() while true do - driver.ssdp_task:get_player_info(player_info_tx, mac_addr) - local recv_ready, _, select_err = cosock.socket.select({ player_info_rx }, nil, nil) - - if type(recv_ready) == "table" and recv_ready[1] == player_info_rx then - local info, recv_err = player_info_rx:receive() - if not info then - device.log.warn(string.format("error receiving device info: %s", recv_err)) - else - ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } - local auth_success, api_key_or_err = driver:check_auth(info) - if not auth_success then - device:offline() - if auth_success == false and api_version >= 14 then - local token_event_receive = driver:oauth_token_event_subscribe() - if not token_event_receive then - log.error("token event bus closed, aborting initialization") - return - end - token_event_receive:settimeout(30) - local token, token_recv_err - -- max 30 mins - local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn(string.format("Error sending token request: %s", request_token_err)) + if driver.ssdp_task then + driver.ssdp_task:get_player_info(player_info_tx, mac_addr) + local recv_ready, _, select_err = cosock.socket.select({ player_info_rx }, nil, nil) + + if type(recv_ready) == "table" and recv_ready[1] == player_info_rx then + local info, recv_err = player_info_rx:receive() + if not info then + device.log.warn(string.format("error receiving device info: %s", recv_err)) + else + ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } + local auth_success, api_key_or_err = driver:check_auth(info) + if not auth_success then + device:offline() + if auth_success == false and api_version >= 14 then + local token_event_receive = driver:oauth_token_event_subscribe() + if not token_event_receive then + log.error("token event bus closed, aborting initialization") + return end - end - - local backoff_timer = nil - while not token do - local send_request = false - -- we use the backoff to create a timer and utilize a select loop here, instead of - -- utilizing a sleep, so that we can create a long delay on our polling of the cloud - -- without putting ourselves in a situation where we're sleeping for an extended period - -- of time so that we don't sleep through the users's log-in attempt and fail to resume - -- our connection attempts in a timely manner. - -- - -- The backoff caps at 30 mins, as commented above - if not backoff_timer then - backoff_timer = cosock.timer.create_oneshot(backoff_builder()) + token_event_receive:settimeout(30) + local token, token_recv_err + -- max 30 mins + local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) + if not driver:is_waiting_for_oauth_token() then + local _, request_token_err = driver:request_oauth_token() + if request_token_err then + log.warn(string.format("Error sending token request: %s", request_token_err)) + end end - local token_recv_ready, _, token_select_err = - cosock.socket.select({ token_event_receive, backoff_timer }, nil, nil) - if token_select_err then - log.warn(string.format("select error: %s", token_select_err)) - end + local backoff_timer = nil + while not token do + local send_request = false + -- we use the backoff to create a timer and utilize a select loop here, instead of + -- utilizing a sleep, so that we can create a long delay on our polling of the cloud + -- without putting ourselves in a situation where we're sleeping for an extended period + -- of time so that we don't sleep through the users's log-in attempt and fail to resume + -- our connection attempts in a timely manner. + -- + -- The backoff caps at 30 mins, as commented above + if not backoff_timer then + backoff_timer = cosock.timer.create_oneshot(backoff_builder()) + end + local token_recv_ready, _, token_select_err = + cosock.socket.select({ token_event_receive, backoff_timer }, nil, nil) - token, token_recv_err = nil, nil - for _, receiver in pairs(token_recv_ready or {}) do - if receiver == backoff_timer then - -- we just make a note that the backoff has elapsed, rather than - -- put a request in flight immediately. - -- - -- This is just in case both receivers are ready, so that we can prioritize - -- handling the token instead of putting another request in flight. - send_request = true - backoff_timer:handled() - backoff_timer = nil + if token_select_err then + log.warn(string.format("select error: %s", token_select_err)) end - if receiver == token_event_receive then - token, token_recv_err = token_event_receive:receive() + token, token_recv_err = nil, nil + for _, receiver in pairs(token_recv_ready or {}) do + if receiver == backoff_timer then + -- we just make a note that the backoff has elapsed, rather than + -- put a request in flight immediately. + -- + -- This is just in case both receivers are ready, so that we can prioritize + -- handling the token instead of putting another request in flight. + send_request = true + backoff_timer:handled() + backoff_timer = nil + end + + if receiver == token_event_receive then + token, token_recv_err = token_event_receive:receive() + end end - end - if token_recv_err == "timeout" then - log.debug("timeout waiting for OAuth token in reconnect task") - elseif token_recv_err and not token then - log.warn( - string.format( - "Unexpected error on token event receive bus: %s", - token_recv_err + if token_recv_err == "timeout" then + log.debug("timeout waiting for OAuth token in reconnect task") + elseif token_recv_err and not token then + log.warn( + string.format( + "Unexpected error on token event receive bus: %s", + token_recv_err + ) ) - ) - end + end - if send_request then - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn( - string.format("Error sending token request: %s", request_token_err) - ) + if send_request then + if not driver:is_waiting_for_oauth_token() then + local _, request_token_err = driver:request_oauth_token() + if request_token_err then + log.warn( + string.format("Error sending token request: %s", request_token_err) + ) + end end end end + else + device.log.error( + string.format( + "error while checking authentication: %s, marking device offline", + api_key_or_err + ) + ) end else - device.log.error( - string.format( - "error while checking authentication: %s, marking device offline", - api_key_or_err - ) + local success, error, error_code = + driver:handle_player_discovery_info(api_key_or_err, info, device) + if success then + return + end + log.error_with( + { hub_logs = true }, + "Error handling Sonos player initialization: %s, error code: %s", + error, + (error_code or "N/A") ) end - else - local success, error, error_code = - driver:handle_player_discovery_info(api_key_or_err, info, device) - if success then - return - end - log.error( - "Error handling Sonos player initialization: %s, error code: %s", - error, - (error_code or "N/A") - ) end + else + device.log.warn( + string.format("select error waiting for initialization device info: %s", select_err) + ) end else - device.log.warn( - string.format("select error waiting for initialization device info: %s", select_err) + device.log.error_with( + { hub_logs = true }, + string.format("Driver wasn't able to spin up SSDP task, cannot initialize devices.") ) end end diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 2bd4e7724d..36614b0915 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -4,7 +4,6 @@ local cosock = require "cosock" local json = require "st.json" local log = require "log" local net_url = require "net.url" -local security = require "st.security" local st_utils = require "st.utils" local sonos_ssdp = require "api.sonos_ssdp_discovery" @@ -17,6 +16,15 @@ local SonosDisco = require "disco" local SonosDriverLifecycleHandlers = require "lifecycle_handlers" local SonosState = require "sonos_state" +local security_load_success, security = pcall(require, "st.security") +if not security_load_success then + log.warn_with( + { hub_logs = true }, + string.format("Unable to load `st.security` module: %s", security) + ) + security = nil +end + local ONE_HOUR_IN_SECONDS = 3600 ---@class SonosDriver: Driver @@ -94,13 +102,13 @@ end ---@return boolean function SonosDriver:is_waiting_for_oauth_token() - return (api_version >= 14) and self.waiting_for_oauth_token + return (api_version >= 14 and security ~= nil) and self.waiting_for_oauth_token end ---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed ---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success function SonosDriver:oauth_token_event_subscribe() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end return self.oauth_token_bus:subscribe() @@ -199,7 +207,7 @@ function SonosDriver:handle_startup_state_received() end function SonosDriver:get_fallback_api_key() - if api_version < 14 then + if api_version < 14 or security == nil then return SonosApi.api_keys.s1_key end @@ -218,7 +226,7 @@ end function SonosDriver:check_auth(info_or_device) local maybe_token, _ = self:get_oauth_token() - local token_valid = (api_version >= 14) + local token_valid = (api_version >= 14 and security ~= nil) and self.oauth and self.oauth.endpoint_app_info and self.oauth.endpoint_app_info.state == "connected" @@ -280,30 +288,44 @@ function SonosDriver:check_auth(info_or_device) return true, api_key end end + + local unauthorized = false for _, api_key in pairs(SonosApi.api_keys) do local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) + + if response._objectType == "globalError" then + unauthorized = (response.errorCode == "ERROR_NOT_AUTHORIZED") + if not unauthorized then + return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) + end + end + if not response or response_err then return nil, string.format("Error while making REST API call: %s", (response_err or "")) end - if response._objectType == "globalError" and response.errorCode ~= "ERROR_NOT_AUTHORIZED" then - return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) - end - if response._objectType == "groups" then return true, api_key end end - return false + if unauthorized then + return false + end + + return nil, + string.format( + "Unable to determine Authentication Status for %s", + st_utils.stringify_table(info_or_device) + ) end ---@return any? ret nil on permissions violation ---@return string? error nil on success function SonosDriver:request_oauth_token() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end local maybe_token, maybe_err = self:get_oauth_token() @@ -324,7 +346,7 @@ end ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ---@return "token expired"|"no token"|"not supported"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() - if api_version < 14 then + if api_version < 14 or security == nil then return nil, "not supported" end self.hub_augmented_driver_data = self.hub_augmented_driver_data or {} @@ -389,6 +411,8 @@ local function make_ssdp_event_handler( log.warn(string.format("Error on token event bus receive: %s", receive_err)) else for _, event in pairs(unauthorized) do + -- shouldn't need a nil check on the ssdp_task here since this whole function + -- won't get called unless the task is successfully spawned. driver.ssdp_task:publish(event) end unauthorized = {} @@ -410,7 +434,10 @@ local function make_ssdp_event_handler( if err_code == "ERROR_NOT_AUTHORIZED" then unauthorized[unique_key] = event end - log.warn(string.format("Failed to handle discovered speaker: %s", handle_err)) + log.warn_with( + { hub_logs = true }, + string.format("Failed to handle discovered speaker: %s", handle_err) + ) else discovered[unique_key] = true end @@ -431,7 +458,7 @@ end function SonosDriver:start_ssdp_event_task() local ssdp_task, err = sonos_ssdp.spawn_persistent_ssdp_task() if err then - log.error(string.format("Unable to create SSDP task: %s", err)) + log.error_with({ hub_logs = true }, string.format("Unable to create SSDP task: %s", err)) end if ssdp_task then self.ssdp_task = ssdp_task @@ -535,6 +562,9 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end if not device_mac_addr then + if not (info and info.discovery_info and info.discovery_info.device) then + return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") + end device_mac_addr = utils.extract_mac_addr(info.discovery_info.device) end diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index e2b4cbd7ed..d9b6b7f0b4 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -183,8 +183,17 @@ end function SonosState:update_device_record_group_info(household, group, device) local player_id = device:get_field(PlayerFields.PLAYER_ID) local group_role - if (player_id and group and group.id and group.coordinatorId) and player_id == group.coordinatorId then - local player_ids_list = household.groups[group.id].playerIds or {} + if + ( + type(household) == "table" + and type(household.groups) == "table" + and player_id + and group + and group.id + and group.coordinatorId + ) and player_id == group.coordinatorId + then + local player_ids_list = (household.groups[group.id] or {}).playerIds or {} if #player_ids_list > 1 then group_role = "primary" else diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua index c96be3c387..7e3b06a198 100644 --- a/drivers/SmartThings/sonos/src/ssdp.lua +++ b/drivers/SmartThings/sonos/src/ssdp.lua @@ -294,7 +294,8 @@ function _ssdp_mt:next_msearch_response() if possible_locations[recv_ip_or_err] == nil then return Err( string.format( - "IP addres [%s] from socket receivefrom doesn't match any of the reply locations: %s", + "IP address [%s] from socket receivefrom doesn't match any of the reply locations: %s", + recv_ip_or_err, table.concat(location_candidates, ", ") ) ) @@ -425,8 +426,11 @@ end function Ssdp.new_search_instance(mx) local udp_sock, sock_err = socket.udp() if sock_err or not udp_sock then - log.error(string.format("Error opening UDP socket for SSDP search: %s", sock_err)) - return nil, "sock_err" + log.error_with( + { hub_logs = true }, + string.format("Error opening UDP socket for SSDP search: %s", sock_err) + ) + return nil, sock_err end local listen_ip = "0.0.0.0" @@ -435,7 +439,10 @@ function Ssdp.new_search_instance(mx) local _, bind_err = udp_sock:setsockname(listen_ip, listen_port) if bind_err then - log.error(string.format("Unable to bind UDP socket for SSDP search: %s", bind_err)) + log.error_with( + { hub_logs = true }, + string.format("Unable to bind UDP socket for SSDP search: %s", bind_err) + ) return nil, bind_err end diff --git a/drivers/SmartThings/sonos/src/utils.lua b/drivers/SmartThings/sonos/src/utils.lua index b88daad6ca..5ffb3a8a9d 100644 --- a/drivers/SmartThings/sonos/src/utils.lua +++ b/drivers/SmartThings/sonos/src/utils.lua @@ -1,4 +1,5 @@ local log = require "log" +local st_utils = require "st.utils" local PlayerFields = require "fields".SonosPlayerFields @@ -125,14 +126,47 @@ end ---@param tbl table ---@param key string local function __case_insensitive_key_index(tbl, key) - assert(type(key) == "string", "key for CaseInsensitiveKeyTable must be a string!") - local lowercase = key:lower() - return rawget(tbl, lowercase) + if type(key) ~= "string" then + local fmt_val + if type(key) == "table" then + fmt_val = st_utils.stringify_table(key) + else + fmt_val = key or "" + end + log.warn_with( + { hub_logs = true }, + string.format( + "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", + fmt_val, + type(key) + ) + ) + return nil + else + local lowercase = key:lower() + return rawget(tbl, lowercase) + end end local function __case_insensitive_key_newindex(tbl, key, value) - assert(type(key) == "string", "key for CaseInsensitiveKeyTable must be a string!") - rawset(tbl, key:lower(), value) + if type(key) ~= "string" then + local fmt_val + if type(key) == "table" then + fmt_val = st_utils.stringify_table(key) + else + fmt_val = key or "" + end + log.warn_with( + { hub_logs = true }, + string.format( + "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", + fmt_val, + type(key) + ) + ) + else + rawset(tbl, key:lower(), value) + end end local _case_insensitive_key_mt = { @@ -147,6 +181,12 @@ end ---@param sonos_device_info SonosDeviceInfo function utils.extract_mac_addr(sonos_device_info) + if type(sonos_device_info) ~= "table" or type(sonos_device_info.serialNumber) ~= "string" then + log.error_with( + { hub_logs = true }, + string.format("Bad sonos device info passed to `extract_mac_addr`: %s", sonos_device_info) + ) + end local mac, _ = sonos_device_info.serialNumber:match("(.*):.*"):gsub("-", "") return utils.normalize_mac_address(mac) end From ca9d0c166085c31ba6a111cf55bdc2f781115c42 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Wed, 25 Jun 2025 11:55:24 -0500 Subject: [PATCH 030/449] Mitigate deserialization errors due to hub 0.57 FW bug 0.57 FW has an extra byte as the first byte of zdo message body payloads and is missing the last byte of the body of the payload. This adds a mitigation in the ZdoMessageBody deserialization code which overrides the Lua library function that corrects the off by 1 error and inserts a default last byte of 0x01 for every Zdo message payload. --- .../zigbee-button/src/st/zigbee/zdo/init.lua | 155 ++++++++++++++++++ .../src/st/zigbee/zdo/init.lua | 155 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua diff --git a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua new file mode 100644 index 0000000000..9d009d47cb --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua @@ -0,0 +1,155 @@ +-- Copyright 2021 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 data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" +local zdo_commands = require "st.zigbee.zdo.commands" + +local zdo_messages = {} + +--- A class representing a zigbee ZDO header +--- @class st.zigbee.zdo.Header +--- +--- @field public NAME string "ZDOHeader" used for printing +--- @field public seqno st.zigbee.data_types.Uint8 the sequence number for the Zigbee message +local ZdoHeader = { + NAME = "ZDOHeader", +} +ZdoHeader.__index = ZdoHeader +zdo_messages.ZdoHeader = ZdoHeader + +--- A function to take a stream of bytes and parse a Zigbee Zdo header +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.Header a new instance of the ZdoHeader parsed from the bytes +function ZdoHeader.deserialize(buf) + local s = {} + s.seqno = data_types.Uint8.deserialize(buf, "seqno") + setmetatable(s, ZdoHeader) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoHeader:get_fields() + return { self.seqno } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoHeader:get_length() end +ZdoHeader.get_length = utils.length_from_fields + +--- A function for serializing this ZdoHeader +--- @return string This message frame serialized +ZdoHeader._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoHeader:pretty_print() end +ZdoHeader.pretty_print = utils.print_from_fields +ZdoHeader.__tostring = ZdoHeader.pretty_print + +--- This is a function to build a Zdo header from its individual components +--- @param orig table UNUSED This is the ZclMessageRx object when called with the syntax ZclMessageRx(...) +--- @param seqno st.zigbee.data_types.Uint8 the sequence number for the ZDO header +--- @return st.zigbee.zdo.Header The constructed ZdoHeader +function ZdoHeader.from_values(orig, seqno) + local out = {} + out.seqno = data_types.validate_or_build_type(seqno, data_types.Uint8, "seqno") + out.seqno.field_name = "seqno" + setmetatable(out, ZdoHeader) + return out +end + +setmetatable(zdo_messages.ZdoHeader, { __call = zdo_messages.ZdoHeader.from_values }) + +--- A class representing a received zigbee ZDO message (device -> hub). +--- @class st.zigbee.zdo.MessageBody +--- +--- @field public NAME string "ZdoMessageRx" used for printing +--- @field public type st.zigbee.data_types.Uint8 message type (internal use only) +--- @field public address_header st.zigbee.AddressHeader The addressing information for this message +--- @field public lqi st.zigbee.data_types.Uint8 The lqi of this message +--- @field public rssi st.zigbee.data_types.Int8 The rssi of this message +--- @field public zdo_header st.zigbee.zdo.Header The ZdoHeader for this message +--- @field public body st.zigbee.zdo.MessageBody The message body. This is a message frame that must implement serialize, get_length, pretty_print, etc. +local ZdoMessageBody = { + NAME = "ZDOMessageBody", +} +ZdoMessageBody.__index = ZdoMessageBody +zdo_messages.ZdoMessageBody = ZdoMessageBody + +--- A function to take a stream of bytes and parse a received Zigbee Zdo message (device -> hub) +--- @param parent table A parent table for the class that includes the type, address_header, lqi, rssi, and body_length +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.MessageBody a new instance of the ZdoMessageRx parsed from the bytes +function ZdoMessageBody.deserialize(parent, buf) + local s = {} + s.zdo_header = zdo_messages.ZdoHeader.deserialize(buf) + -- there is a bug in hub FW that causes the zdo message payload to have an invalid first byte + -- and a missing last byte. Default to adding in a 1, which is a good default for the mgmt_bind_response + -- binding table entry endpoint_id + local version = require "version" + if version.rpc == 8 then + buf.buf = buf.buf .. "\01" + buf:seek(1) + end + s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) + + setmetatable(s, ZdoMessageBody) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoMessageBody:get_fields() + return { + self.zdo_header, + self.zdo_body + } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoMessageBody:get_length() end +ZdoMessageBody.get_length = utils.length_from_fields + +--- A function for serializing this Zdo Message +--- @return string the bytes representing this message +ZdoMessageBody._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoMessageBody:pretty_print() end +ZdoMessageBody.pretty_print = utils.print_from_fields +ZdoMessageBody.__tostring = ZdoMessageBody.pretty_print + +--- This is a function to build an zdo rx message from its individual components +--- @param orig table UNUSED This is the ZdoMessageRx object when called with the syntax ZdoMessageRx(...) +--- @param data_table table a table containing the fields of this ZdoMessageRx. address_header, zdo_header, and body are required. type, lqi, rssi are optional with defaults +--- @return st.zigbee.zdo.MessageBody The constructed ZdoMessageRx +function ZdoMessageBody.from_values(orig, data_table) + if data_table.zdo_header == nil then + -- Just set seqno to 0 + data_table.zdo_header = zdo_messages.ZdoHeader(0) + end + if data_table.zdo_body == nil then + error(string.format("%s requires valid body", orig.NAME), 2) + end + setmetatable(data_table, ZdoMessageBody) + return data_table +end + +setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) + +return zdo_messages \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua new file mode 100644 index 0000000000..9d009d47cb --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua @@ -0,0 +1,155 @@ +-- Copyright 2021 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 data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" +local zdo_commands = require "st.zigbee.zdo.commands" + +local zdo_messages = {} + +--- A class representing a zigbee ZDO header +--- @class st.zigbee.zdo.Header +--- +--- @field public NAME string "ZDOHeader" used for printing +--- @field public seqno st.zigbee.data_types.Uint8 the sequence number for the Zigbee message +local ZdoHeader = { + NAME = "ZDOHeader", +} +ZdoHeader.__index = ZdoHeader +zdo_messages.ZdoHeader = ZdoHeader + +--- A function to take a stream of bytes and parse a Zigbee Zdo header +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.Header a new instance of the ZdoHeader parsed from the bytes +function ZdoHeader.deserialize(buf) + local s = {} + s.seqno = data_types.Uint8.deserialize(buf, "seqno") + setmetatable(s, ZdoHeader) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoHeader:get_fields() + return { self.seqno } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoHeader:get_length() end +ZdoHeader.get_length = utils.length_from_fields + +--- A function for serializing this ZdoHeader +--- @return string This message frame serialized +ZdoHeader._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoHeader:pretty_print() end +ZdoHeader.pretty_print = utils.print_from_fields +ZdoHeader.__tostring = ZdoHeader.pretty_print + +--- This is a function to build a Zdo header from its individual components +--- @param orig table UNUSED This is the ZclMessageRx object when called with the syntax ZclMessageRx(...) +--- @param seqno st.zigbee.data_types.Uint8 the sequence number for the ZDO header +--- @return st.zigbee.zdo.Header The constructed ZdoHeader +function ZdoHeader.from_values(orig, seqno) + local out = {} + out.seqno = data_types.validate_or_build_type(seqno, data_types.Uint8, "seqno") + out.seqno.field_name = "seqno" + setmetatable(out, ZdoHeader) + return out +end + +setmetatable(zdo_messages.ZdoHeader, { __call = zdo_messages.ZdoHeader.from_values }) + +--- A class representing a received zigbee ZDO message (device -> hub). +--- @class st.zigbee.zdo.MessageBody +--- +--- @field public NAME string "ZdoMessageRx" used for printing +--- @field public type st.zigbee.data_types.Uint8 message type (internal use only) +--- @field public address_header st.zigbee.AddressHeader The addressing information for this message +--- @field public lqi st.zigbee.data_types.Uint8 The lqi of this message +--- @field public rssi st.zigbee.data_types.Int8 The rssi of this message +--- @field public zdo_header st.zigbee.zdo.Header The ZdoHeader for this message +--- @field public body st.zigbee.zdo.MessageBody The message body. This is a message frame that must implement serialize, get_length, pretty_print, etc. +local ZdoMessageBody = { + NAME = "ZDOMessageBody", +} +ZdoMessageBody.__index = ZdoMessageBody +zdo_messages.ZdoMessageBody = ZdoMessageBody + +--- A function to take a stream of bytes and parse a received Zigbee Zdo message (device -> hub) +--- @param parent table A parent table for the class that includes the type, address_header, lqi, rssi, and body_length +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.MessageBody a new instance of the ZdoMessageRx parsed from the bytes +function ZdoMessageBody.deserialize(parent, buf) + local s = {} + s.zdo_header = zdo_messages.ZdoHeader.deserialize(buf) + -- there is a bug in hub FW that causes the zdo message payload to have an invalid first byte + -- and a missing last byte. Default to adding in a 1, which is a good default for the mgmt_bind_response + -- binding table entry endpoint_id + local version = require "version" + if version.rpc == 8 then + buf.buf = buf.buf .. "\01" + buf:seek(1) + end + s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) + + setmetatable(s, ZdoMessageBody) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoMessageBody:get_fields() + return { + self.zdo_header, + self.zdo_body + } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoMessageBody:get_length() end +ZdoMessageBody.get_length = utils.length_from_fields + +--- A function for serializing this Zdo Message +--- @return string the bytes representing this message +ZdoMessageBody._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoMessageBody:pretty_print() end +ZdoMessageBody.pretty_print = utils.print_from_fields +ZdoMessageBody.__tostring = ZdoMessageBody.pretty_print + +--- This is a function to build an zdo rx message from its individual components +--- @param orig table UNUSED This is the ZdoMessageRx object when called with the syntax ZdoMessageRx(...) +--- @param data_table table a table containing the fields of this ZdoMessageRx. address_header, zdo_header, and body are required. type, lqi, rssi are optional with defaults +--- @return st.zigbee.zdo.MessageBody The constructed ZdoMessageRx +function ZdoMessageBody.from_values(orig, data_table) + if data_table.zdo_header == nil then + -- Just set seqno to 0 + data_table.zdo_header = zdo_messages.ZdoHeader(0) + end + if data_table.zdo_body == nil then + error(string.format("%s requires valid body", orig.NAME), 2) + end + setmetatable(data_table, ZdoMessageBody) + return data_table +end + +setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) + +return zdo_messages \ No newline at end of file From 2df094a7270abaa480a449772fa82382d544d68f Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 25 Jun 2025 17:22:18 -0500 Subject: [PATCH 031/449] fix: Another nil index along an error handling path --- drivers/SmartThings/sonos/src/sonos_driver.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 36614b0915..c603a7f399 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -294,7 +294,7 @@ function SonosDriver:check_auth(info_or_device) local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) - if response._objectType == "globalError" then + if response and response._objectType == "globalError" then unauthorized = (response.errorCode == "ERROR_NOT_AUTHORIZED") if not unauthorized then return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) From 22128bea0b933f34d4e023bd8193d3056b3ea983 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Wed, 25 Jun 2025 11:55:24 -0500 Subject: [PATCH 032/449] Mitigate deserialization errors due to hub 0.57 FW bug 0.57 FW has an extra byte as the first byte of zdo message body payloads and is missing the last byte of the body of the payload. This adds a mitigation in the ZdoMessageBody deserialization code which overrides the Lua library function that corrects the off by 1 error and inserts a default last byte of 0x01 for every Zdo message payload. --- .../zigbee-button/src/st/zigbee/zdo/init.lua | 155 ++++++++++++++++++ .../src/st/zigbee/zdo/init.lua | 155 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua diff --git a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua new file mode 100644 index 0000000000..9d009d47cb --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua @@ -0,0 +1,155 @@ +-- Copyright 2021 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 data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" +local zdo_commands = require "st.zigbee.zdo.commands" + +local zdo_messages = {} + +--- A class representing a zigbee ZDO header +--- @class st.zigbee.zdo.Header +--- +--- @field public NAME string "ZDOHeader" used for printing +--- @field public seqno st.zigbee.data_types.Uint8 the sequence number for the Zigbee message +local ZdoHeader = { + NAME = "ZDOHeader", +} +ZdoHeader.__index = ZdoHeader +zdo_messages.ZdoHeader = ZdoHeader + +--- A function to take a stream of bytes and parse a Zigbee Zdo header +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.Header a new instance of the ZdoHeader parsed from the bytes +function ZdoHeader.deserialize(buf) + local s = {} + s.seqno = data_types.Uint8.deserialize(buf, "seqno") + setmetatable(s, ZdoHeader) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoHeader:get_fields() + return { self.seqno } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoHeader:get_length() end +ZdoHeader.get_length = utils.length_from_fields + +--- A function for serializing this ZdoHeader +--- @return string This message frame serialized +ZdoHeader._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoHeader:pretty_print() end +ZdoHeader.pretty_print = utils.print_from_fields +ZdoHeader.__tostring = ZdoHeader.pretty_print + +--- This is a function to build a Zdo header from its individual components +--- @param orig table UNUSED This is the ZclMessageRx object when called with the syntax ZclMessageRx(...) +--- @param seqno st.zigbee.data_types.Uint8 the sequence number for the ZDO header +--- @return st.zigbee.zdo.Header The constructed ZdoHeader +function ZdoHeader.from_values(orig, seqno) + local out = {} + out.seqno = data_types.validate_or_build_type(seqno, data_types.Uint8, "seqno") + out.seqno.field_name = "seqno" + setmetatable(out, ZdoHeader) + return out +end + +setmetatable(zdo_messages.ZdoHeader, { __call = zdo_messages.ZdoHeader.from_values }) + +--- A class representing a received zigbee ZDO message (device -> hub). +--- @class st.zigbee.zdo.MessageBody +--- +--- @field public NAME string "ZdoMessageRx" used for printing +--- @field public type st.zigbee.data_types.Uint8 message type (internal use only) +--- @field public address_header st.zigbee.AddressHeader The addressing information for this message +--- @field public lqi st.zigbee.data_types.Uint8 The lqi of this message +--- @field public rssi st.zigbee.data_types.Int8 The rssi of this message +--- @field public zdo_header st.zigbee.zdo.Header The ZdoHeader for this message +--- @field public body st.zigbee.zdo.MessageBody The message body. This is a message frame that must implement serialize, get_length, pretty_print, etc. +local ZdoMessageBody = { + NAME = "ZDOMessageBody", +} +ZdoMessageBody.__index = ZdoMessageBody +zdo_messages.ZdoMessageBody = ZdoMessageBody + +--- A function to take a stream of bytes and parse a received Zigbee Zdo message (device -> hub) +--- @param parent table A parent table for the class that includes the type, address_header, lqi, rssi, and body_length +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.MessageBody a new instance of the ZdoMessageRx parsed from the bytes +function ZdoMessageBody.deserialize(parent, buf) + local s = {} + s.zdo_header = zdo_messages.ZdoHeader.deserialize(buf) + -- there is a bug in hub FW that causes the zdo message payload to have an invalid first byte + -- and a missing last byte. Default to adding in a 1, which is a good default for the mgmt_bind_response + -- binding table entry endpoint_id + local version = require "version" + if version.rpc == 8 then + buf.buf = buf.buf .. "\01" + buf:seek(1) + end + s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) + + setmetatable(s, ZdoMessageBody) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoMessageBody:get_fields() + return { + self.zdo_header, + self.zdo_body + } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoMessageBody:get_length() end +ZdoMessageBody.get_length = utils.length_from_fields + +--- A function for serializing this Zdo Message +--- @return string the bytes representing this message +ZdoMessageBody._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoMessageBody:pretty_print() end +ZdoMessageBody.pretty_print = utils.print_from_fields +ZdoMessageBody.__tostring = ZdoMessageBody.pretty_print + +--- This is a function to build an zdo rx message from its individual components +--- @param orig table UNUSED This is the ZdoMessageRx object when called with the syntax ZdoMessageRx(...) +--- @param data_table table a table containing the fields of this ZdoMessageRx. address_header, zdo_header, and body are required. type, lqi, rssi are optional with defaults +--- @return st.zigbee.zdo.MessageBody The constructed ZdoMessageRx +function ZdoMessageBody.from_values(orig, data_table) + if data_table.zdo_header == nil then + -- Just set seqno to 0 + data_table.zdo_header = zdo_messages.ZdoHeader(0) + end + if data_table.zdo_body == nil then + error(string.format("%s requires valid body", orig.NAME), 2) + end + setmetatable(data_table, ZdoMessageBody) + return data_table +end + +setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) + +return zdo_messages \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua new file mode 100644 index 0000000000..9d009d47cb --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua @@ -0,0 +1,155 @@ +-- Copyright 2021 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 data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" +local zdo_commands = require "st.zigbee.zdo.commands" + +local zdo_messages = {} + +--- A class representing a zigbee ZDO header +--- @class st.zigbee.zdo.Header +--- +--- @field public NAME string "ZDOHeader" used for printing +--- @field public seqno st.zigbee.data_types.Uint8 the sequence number for the Zigbee message +local ZdoHeader = { + NAME = "ZDOHeader", +} +ZdoHeader.__index = ZdoHeader +zdo_messages.ZdoHeader = ZdoHeader + +--- A function to take a stream of bytes and parse a Zigbee Zdo header +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.Header a new instance of the ZdoHeader parsed from the bytes +function ZdoHeader.deserialize(buf) + local s = {} + s.seqno = data_types.Uint8.deserialize(buf, "seqno") + setmetatable(s, ZdoHeader) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoHeader:get_fields() + return { self.seqno } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoHeader:get_length() end +ZdoHeader.get_length = utils.length_from_fields + +--- A function for serializing this ZdoHeader +--- @return string This message frame serialized +ZdoHeader._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoHeader:pretty_print() end +ZdoHeader.pretty_print = utils.print_from_fields +ZdoHeader.__tostring = ZdoHeader.pretty_print + +--- This is a function to build a Zdo header from its individual components +--- @param orig table UNUSED This is the ZclMessageRx object when called with the syntax ZclMessageRx(...) +--- @param seqno st.zigbee.data_types.Uint8 the sequence number for the ZDO header +--- @return st.zigbee.zdo.Header The constructed ZdoHeader +function ZdoHeader.from_values(orig, seqno) + local out = {} + out.seqno = data_types.validate_or_build_type(seqno, data_types.Uint8, "seqno") + out.seqno.field_name = "seqno" + setmetatable(out, ZdoHeader) + return out +end + +setmetatable(zdo_messages.ZdoHeader, { __call = zdo_messages.ZdoHeader.from_values }) + +--- A class representing a received zigbee ZDO message (device -> hub). +--- @class st.zigbee.zdo.MessageBody +--- +--- @field public NAME string "ZdoMessageRx" used for printing +--- @field public type st.zigbee.data_types.Uint8 message type (internal use only) +--- @field public address_header st.zigbee.AddressHeader The addressing information for this message +--- @field public lqi st.zigbee.data_types.Uint8 The lqi of this message +--- @field public rssi st.zigbee.data_types.Int8 The rssi of this message +--- @field public zdo_header st.zigbee.zdo.Header The ZdoHeader for this message +--- @field public body st.zigbee.zdo.MessageBody The message body. This is a message frame that must implement serialize, get_length, pretty_print, etc. +local ZdoMessageBody = { + NAME = "ZDOMessageBody", +} +ZdoMessageBody.__index = ZdoMessageBody +zdo_messages.ZdoMessageBody = ZdoMessageBody + +--- A function to take a stream of bytes and parse a received Zigbee Zdo message (device -> hub) +--- @param parent table A parent table for the class that includes the type, address_header, lqi, rssi, and body_length +--- @param buf Reader The buf positioned at the beginning of the bytes representing the ZdoHeader +--- @return st.zigbee.zdo.MessageBody a new instance of the ZdoMessageRx parsed from the bytes +function ZdoMessageBody.deserialize(parent, buf) + local s = {} + s.zdo_header = zdo_messages.ZdoHeader.deserialize(buf) + -- there is a bug in hub FW that causes the zdo message payload to have an invalid first byte + -- and a missing last byte. Default to adding in a 1, which is a good default for the mgmt_bind_response + -- binding table entry endpoint_id + local version = require "version" + if version.rpc == 8 then + buf.buf = buf.buf .. "\01" + buf:seek(1) + end + s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) + + setmetatable(s, ZdoMessageBody) + return s +end + +--- A helper function used by common code to get all the component pieces of this message frame +---@return table An array formatted table with each component field in the order their bytes should be serialized +function ZdoMessageBody:get_fields() + return { + self.zdo_header, + self.zdo_body + } +end + +--- A function to return the total length in bytes this frame uses when serialized +--- @return number the length in bytes of this frame +function ZdoMessageBody:get_length() end +ZdoMessageBody.get_length = utils.length_from_fields + +--- A function for serializing this Zdo Message +--- @return string the bytes representing this message +ZdoMessageBody._serialize = utils.serialize_from_fields + +--- A function for printing in a human readable format +--- @return string A human readable representation of this message frame +function ZdoMessageBody:pretty_print() end +ZdoMessageBody.pretty_print = utils.print_from_fields +ZdoMessageBody.__tostring = ZdoMessageBody.pretty_print + +--- This is a function to build an zdo rx message from its individual components +--- @param orig table UNUSED This is the ZdoMessageRx object when called with the syntax ZdoMessageRx(...) +--- @param data_table table a table containing the fields of this ZdoMessageRx. address_header, zdo_header, and body are required. type, lqi, rssi are optional with defaults +--- @return st.zigbee.zdo.MessageBody The constructed ZdoMessageRx +function ZdoMessageBody.from_values(orig, data_table) + if data_table.zdo_header == nil then + -- Just set seqno to 0 + data_table.zdo_header = zdo_messages.ZdoHeader(0) + end + if data_table.zdo_body == nil then + error(string.format("%s requires valid body", orig.NAME), 2) + end + setmetatable(data_table, ZdoMessageBody) + return data_table +end + +setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) + +return zdo_messages \ No newline at end of file From bf5bfd46a9e79dd47456414ad9d93d7533d58ea7 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 25 Jun 2025 17:22:18 -0500 Subject: [PATCH 033/449] fix: Another nil index along an error handling path --- drivers/SmartThings/sonos/src/sonos_driver.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 36614b0915..c603a7f399 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -294,7 +294,7 @@ function SonosDriver:check_auth(info_or_device) local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) - if response._objectType == "globalError" then + if response and response._objectType == "globalError" then unauthorized = (response.errorCode == "ERROR_NOT_AUTHORIZED") if not unauthorized then return nil, string.format("Unexpected error body: %s", st_utils.stringify_table(response)) From 370e2c32dc03b119d94a60ec8e38a572b4ec9f81 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 1 Jul 2025 15:52:02 -0500 Subject: [PATCH 034/449] sonos: fix infinite recursive reconnect tasks --- .../SmartThings/sonos/src/api/sonos_connection.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 1ae8465915..f1773e24aa 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -27,6 +27,7 @@ local utils = require "utils" --- @field package _self_listener_uuid string --- @field package _coord_listener_uuid string --- @field package _initialized boolean +--- @field package _reconnecting boolean if a reconnect task is active --- @field package on_message fun(...)? --- @field package on_error fun(...)? --- @field package on_close fun(...)? @@ -229,10 +230,12 @@ local function _legacy_reconnect_task(sonos_conn) while not sonos_conn:is_running() do local start_success = sonos_conn:start() if start_success then + sonos_conn._reconnecting = false return end cosock.socket.sleep(backoff()) end + sonos_conn._reconnecting = false end, string.format("%s Reconnect Task", sonos_conn.device.label)) end @@ -263,6 +266,7 @@ local function _oauth_reconnect_task(sonos_conn) else local start_success = sonos_conn:start() if start_success then + sonos_conn._reconnecting = false return end end @@ -275,11 +279,16 @@ local function _oauth_reconnect_task(sonos_conn) end cosock.socket.sleep(backoff()) end + sonos_conn._reconnecting = false end, string.format("%s Reconnect Task", sonos_conn.device.label)) end ---@param sonos_conn SonosConnection local function _spawn_reconnect_task(sonos_conn) + if sonos_conn._reconnecting then + return + end + sonos_conn._reconnecting = true if type(api_version) == "number" and api_version >= 14 then _oauth_reconnect_task(sonos_conn) else @@ -294,7 +303,7 @@ end function SonosConnection.new(driver, device) log.debug(string.format("Creating new SonosConnection for %s", device.label)) local self = setmetatable( - { driver = driver, device = device, _listener_uuids = {}, _initialized = false }, + { driver = driver, device = device, _listener_uuids = {}, _initialized = false, _reconnecting = false }, SonosConnection ) From 04b3b7ed5e38f88b83e62a5b0433ac6ac1288569 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 8 Jul 2025 12:17:02 -0700 Subject: [PATCH 035/449] CHAD-15418 workaround endpoint ids change from numbers to strings shortly after install. This also fixes some incorrectly-addressed fields for kickstarter sensors. --- .../zigbee-contact/src/smartsense-multi/init.lua | 3 ++- .../zigbee-motion-sensor/src/smartsense/init.lua | 3 ++- .../src/test/test_smartsense_motion_sensor.lua | 2 +- .../zigbee-switch/src/test/test_zll_color_temp_bulb.lua | 2 +- .../SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua | 2 +- .../zigbee-switch/src/test/test_zll_dimmer_bulb.lua | 2 +- .../zigbee-switch/src/test/test_zll_rgb_bulb.lua | 2 +- .../zigbee-switch/src/test/test_zll_rgbw_bulb.lua | 2 +- drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua | 6 ++++-- 9 files changed, 14 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua index 92fa777b7c..cbe59d7c09 100644 --- a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua @@ -37,7 +37,8 @@ local function can_handle(opts, driver, device, ...) return true end end - if device.zigbee_endpoints[1].profileId == SMARTSENSE_PROFILE_ID then return true end + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] + if endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end return false end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua index c1c3912db4..78f570872f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua @@ -44,8 +44,9 @@ local battery_table = { } local function can_handle(opts, driver, device, ...) + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] if (device:get_manufacturer() == SMARTSENSE_MFR and device:get_model() == SMARTSENSE_MODEL) or - device.zigbee_endpoints[1].profileId == SMARTSENSE_PROFILE_ID then + endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end return false 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 64a5b01dfe..b61abdec38 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 @@ -33,7 +33,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "SmartThings", model = "PGC314", - profileId = 0xFC01, + profile_id = 0xFC01, server_clusters = {} } } 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 e1a2c2645d..6d7986700e 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 @@ -30,7 +30,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( manufacturer = "IKEA of Sweden", model = "TRADFRI bulb E26 WS clear 950lm", server_clusters = { 0x0006, 0x0008, 0x0300 }, - profile = 0xC05E, + profile_id = 0xC05E, } } } 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 1cd4766025..eb590c3874 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua @@ -30,7 +30,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( manufacturer = "Leviton", model = "DL6HD", server_clusters = { 0x0006, 0x0008 }, - profile = 0xC05E, + profile_id = 0xC05E, } } } 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 f382b0b224..922a0ce364 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 @@ -29,7 +29,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( manufacturer = "AduroSmart Eria", model = "ZLL-DimmableLight", server_clusters = { 0x0006, 0x0008 }, - profile = 0xC05E, + profile_id = 0xC05E, } } } 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 5391b7fbe4..b58acb6dc2 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 @@ -35,7 +35,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( manufacturer = "IKEA of Sweden", model = "TRADFRI bulb E27 CWS opal 600lm", server_clusters = { 0x0006, 0x0008, 0x0300 }, - profile = 0xC05E, + profile_id = 0xC05E, } } } 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 cc0c35ca84..51edc21777 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 @@ -30,7 +30,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( manufacturer = "AduroSmart Eria", model = "ZLL-ExtendedColor", server_clusters = { 0x0006, 0x0008, 0x0300 }, - profile = 0xC05E, + profile_id = 0xC05E, } } } diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index a2e1351583..59424dd784 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -17,10 +17,12 @@ local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local function zll_profile(opts, driver, device, zb_rx, ...) - if (device.zigbee_endpoints[device.fingerprinted_endpoint_id].profile == constants.ZLL_PROFILE_ID) then + local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] + if (endpoint.profile_id == constants.ZLL_PROFILE_ID) then local subdriver = require("zll-polling") return true, subdriver - else return false + else + return false end end From 78c42672ded8bd846ae948d38c2ce7f9d2f363c1 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 7 Jul 2025 11:48:41 -0700 Subject: [PATCH 036/449] WWSTCERT-6934 Shelly 1L Gen3 WWSTCERT-6937 Shelly 2L Gen3 WWSTCERT-6940 Shelly 1 Mini Gen3 --- .../SmartThings/matter-switch/fingerprints.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index ce06d1a682..20206a0091 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -740,6 +740,21 @@ matterManufacturer: vendorId: 0x1490 productId: 0x1018 deviceProfileName: plug-binary + - id: "5264/4115" + deviceLabel: Shelly 2L Gen3 + vendorId: 0x1490 + productId: 0x1013 + deviceProfileName: plug-binary + - id: "5264/4116" + deviceLabel: Shelly 1L Gen3 + vendorId: 0x1490 + productId: 0x1014 + deviceProfileName: plug-binary + - id: "5264/4117" + deviceLabel: Shelly 1 Mini Gen3 + vendorId: 0x1490 + productId: 0x1015 + deviceProfileName: plug-binary - id: "5264/4118" deviceLabel: Shelly 1PM Mini Gen3 vendorId: 0x1490 From 7874483dd1685f77eac1c80c6be865c6c804190c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 11 Jul 2025 14:49:58 -0700 Subject: [PATCH 037/449] Roll back health check changes for staged release. --- .../src/init.lua | 3 +- ...test_climax_technology_carbon_monoxide.lua | 4 ++- .../src/test/test_zigbee_carbon_monoxide.lua | 4 ++- drivers/SmartThings/zigbee-fan/src/init.lua | 3 +- .../zigbee-fan/src/test/test_fan_light.lua | 4 ++- .../zigbee-humidity-sensor/src/init.lua | 3 +- .../src/test/test_humidity_battery_sensor.lua | 4 ++- .../zigbee-illuminance-sensor/src/init.lua | 3 +- .../src/test/test_illuminance_sensor.lua | 4 ++- .../test/test_illuminance_sensor_aqara.lua | 4 ++- .../zigbee-power-meter/src/init.lua | 3 +- .../src/test/test_zigbee_power_meter.lua | 4 ++- ..._zigbee_power_meter_consumption_report.lua | 4 ++- ...e_power_meter_consumption_report_sihas.lua | 4 ++- .../zigbee-presence-sensor/src/init.lua | 3 +- .../src/test/test_st_arrival_sensor_v1.lua | 8 +++-- .../src/test/test_zigbee_presence_sensor.lua | 8 +++-- .../zigbee-range-extender/src/init.lua | 1 - .../src/test/test_zigbee_extend.lua | 29 ++++++++++--------- drivers/SmartThings/zigbee-siren/src/init.lua | 3 +- .../src/test/test_frient_siren.lua | 4 ++- .../zigbee-siren/src/test/test_ozom_siren.lua | 4 ++- .../zigbee-watering-kit/src/init.lua | 1 - 23 files changed, 68 insertions(+), 44 deletions(-) diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua index 21c170151e..c29b77d546 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua @@ -24,8 +24,7 @@ local zigbee_carbon_monoxide_driver_template = { capabilities.battery, }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, - sub_drivers = { require("ClimaxTechnology") }, - health_check = false, + sub_drivers = { require("ClimaxTechnology") } } defaults.register_for_default_handlers(zigbee_carbon_monoxide_driver_template, 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 a2ba57dfed..07c7a38314 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 @@ -34,7 +34,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) test.register_message_test( 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 313f3ea9ec..4b3172e7b2 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 @@ -29,7 +29,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) test.register_message_test( diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index 1766be9ff1..eff62434ef 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -38,8 +38,7 @@ local zigbee_fan_driver = { }, lifecycle_handlers = { init = device_init - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_fan_driver,zigbee_fan_driver.supported_capabilities) 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 4ef4ce37e6..f83a33074b 100644 --- a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua +++ b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua @@ -39,7 +39,9 @@ local mock_base_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_base_device)end + test.mock_device.add_test_device(mock_base_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) -- create test commands diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index cf136ae1fd..cf1bb61b7c 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -86,8 +86,7 @@ local zigbee_humidity_driver = { require("centralite-sensor"), require("heiman-sensor"), require("frient-sensor") - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities) 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 62f6421fa7..5d6b6981f4 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 @@ -37,7 +37,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 45a3ade62b..ea159c323a 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -23,8 +23,7 @@ local zigbee_illuminance_driver = { }, sub_drivers = { require("aqara") - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_illuminance_driver, zigbee_illuminance_driver.supported_capabilities) 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 02c3c63a55..6f180711d8 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 @@ -35,7 +35,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) 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 11d66478ba..9545664bbf 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 @@ -49,7 +49,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index a12272aada..507e14e4eb 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -57,8 +57,7 @@ local zigbee_power_meter_driver_template = { lifecycle_handlers = { init = device_init, doConfigure = do_configure, - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_power_meter_driver_template, zigbee_power_meter_driver_template.supported_capabilities) 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 afe97fb0f9..c002a6813e 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 @@ -27,7 +27,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua index dcbb1b0b00..905f7015ef 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua @@ -36,7 +36,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) 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 536863b409..d94f709e80 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 @@ -36,7 +36,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index 1ab62e821d..0b4ecf5c75 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -202,8 +202,7 @@ local zigbee_presence_driver = { sub_drivers = { require("aqara"), require("arrival-sensor-v1") - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_presence_driver, zigbee_presence_driver.supported_capabilities) 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 0ddffcedab..513b40966d 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 @@ -76,7 +76,9 @@ local add_device = function() end local function test_init() - test.mock_device.add_test_device(mock_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) @@ -191,7 +193,9 @@ 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 + test_init = function() + zigbee_test_utils.init_noop_health_check_timer() + end } ) 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 30fec61f6d..0749599bb7 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 @@ -46,7 +46,9 @@ local mock_simple_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_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + zigbee_test_utils.init_noop_health_check_timer() +end local function build_config_response_msg(cluster, status) local addr_header = messages.AddressHeader( @@ -199,7 +201,9 @@ 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 + test_init = function() + zigbee_test_utils.init_noop_health_check_timer() + end } ) diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index a8c4a2796a..b41180e2d4 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -30,7 +30,6 @@ local zigbee_range_driver_template = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - health_check = false, } local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbee_range_driver_template) 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 39404005fa..321d96b609 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 @@ -14,6 +14,7 @@ -- 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 t_utils = require "integration_test.utils" local Basic = clusters.Basic @@ -23,7 +24,9 @@ local mock_device = test.mock_device.build_test_zigbee_device( ) local function test_init() - test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(600, "interval", "health_check") + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() + test.timer.__create_and_queue_test_time_advance_timer(600, "interval", "health_check") end test.set_test_init_function(test_init) @@ -42,17 +45,17 @@ test.register_coroutine_test( end ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) --- test.wait_for_events() - --- test.mock_time.advance_time(50000) --- test.socket.zigbee:__set_channel_ordering("relaxed") --- test.socket.zigbee:__expect_send({ mock_device.id, Basic.attributes.ZCLVersion:read(mock_device) }) --- test.wait_for_events() --- end --- ) +test.register_coroutine_test( + "Health check should check all relevant attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + + test.mock_time.advance_time(50000) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ mock_device.id, Basic.attributes.ZCLVersion:read(mock_device) }) + test.wait_for_events() + end +) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index 0f08b067fb..5d3fafac39 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -200,8 +200,7 @@ local zigbee_siren_driver_template = { data_type = IASZone.attributes.ZoneStatus.base_type } } - }, - health_check = false, + } } defaults.register_for_default_handlers(zigbee_siren_driver_template, zigbee_siren_driver_template.supported_capabilities) 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 098b2ab1bd..0323a57e79 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -37,7 +37,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) 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 5b9a6a2bea..f992686439 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua @@ -38,7 +38,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zigbee-watering-kit/src/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/init.lua index f39c04beaa..357f68e719 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/init.lua @@ -13,7 +13,6 @@ local zigbee_water_driver_template = { sub_drivers = { require("thirdreality") }, - health_check = false, } defaults.register_for_default_handlers(zigbee_water_driver_template, zigbee_water_driver_template.supported_capabilities) From eb5b392dbaf264711f6f70aa168e86d165782f7c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 14 Jul 2025 09:41:29 -0700 Subject: [PATCH 038/449] Revert health check changes for non-ST drivers --- drivers/Aqara/aqara-cube/src/init.lua | 3 +- drivers/Aqara/aqara-feeder/src/init.lua | 1 - drivers/Aqara/aqara-lock/src/init.lua | 3 +- drivers/Unofficial/tuya-zigbee/src/init.lua | 4 +- .../src/test/test_meian_button.lua | 80 ++++++++++--------- .../tuya-zigbee/src/test/test_tuya_button.lua | 4 +- .../src/test/test_tuya_curtain.lua | 4 +- .../src/test/test_tuya_motion_sensor.lua | 4 +- .../src/test/test_tuya_smoke_detector.lua | 4 +- .../tuya-zigbee/src/test/test_tuya_switch.lua | 4 +- 10 files changed, 59 insertions(+), 52 deletions(-) diff --git a/drivers/Aqara/aqara-cube/src/init.lua b/drivers/Aqara/aqara-cube/src/init.lua index 81078a59a9..cde24f8f23 100644 --- a/drivers/Aqara/aqara-cube/src/init.lua +++ b/drivers/Aqara/aqara-cube/src/init.lua @@ -125,8 +125,7 @@ local aqara_cube_t1_pro_handler = { lifecycle_handlers = { init = device_init, added = device_added - }, - health_check = false, + } } local aqara_cube_t1_pro_driver = ZigbeeDriver("aqara_cube_t1_pro", aqara_cube_t1_pro_handler) diff --git a/drivers/Aqara/aqara-feeder/src/init.lua b/drivers/Aqara/aqara-feeder/src/init.lua index 7ab9d64ef8..fba75a42c2 100644 --- a/drivers/Aqara/aqara-feeder/src/init.lua +++ b/drivers/Aqara/aqara-feeder/src/init.lua @@ -184,7 +184,6 @@ local aqara_pet_feeder_handler = { infoChanged = device_info_changed, doConfigure = device_configure }, - health_check = false, can_handle = function(opts, driver, device, ...) return device:get_model() == "aqara.feeder.acn001" end diff --git a/drivers/Aqara/aqara-lock/src/init.lua b/drivers/Aqara/aqara-lock/src/init.lua index 0d7cc77c4f..053d78992f 100644 --- a/drivers/Aqara/aqara-lock/src/init.lua +++ b/drivers/Aqara/aqara-lock/src/init.lua @@ -325,8 +325,7 @@ local aqara_locks_handler = { }, secret_data_handlers = { [security.SECRET_KIND_AQARA] = my_secret_data_handler - }, - health_check = false, + } } local aqara_locks_driver = ZigbeeDriver("aqara_locks_k100", aqara_locks_handler) diff --git a/drivers/Unofficial/tuya-zigbee/src/init.lua b/drivers/Unofficial/tuya-zigbee/src/init.lua index 8fb452abb2..d64766f496 100644 --- a/drivers/Unofficial/tuya-zigbee/src/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/init.lua @@ -26,9 +26,7 @@ local unofficial_tuya_driver_template = { require("curtain"), require("motion-sensor"), require("smoke-detector"), - require("switch") - }, - health_check = false, + require("switch")} } defaults.register_for_default_handlers(unofficial_tuya_driver_template, unofficial_tuya_driver_template.supported_capabilities) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua index a97ff0af10..5efe7043fd 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua @@ -40,7 +40,9 @@ local mock_device_meian_button = 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_meian_button)end + test.mock_device.add_test_device(mock_device_meian_button) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) @@ -77,44 +79,44 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added"}) --- test.socket.capability:__expect_send( --- mock_device_meian_button:generate_test_message( --- "main", --- capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) --- ) --- ) --- test.socket.capability:__expect_send( --- mock_device_meian_button:generate_test_message( --- "main", --- capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) --- ) --- ) --- test.socket.capability:__expect_send({ --- mock_device_meian_button.id, --- { --- capability_id = "button", component_id = "main", --- attribute_id = "button", state = { value = "pushed" } --- } --- }) --- test.socket.zigbee:__expect_send({ mock_device_meian_button.id, tuya_utils.build_tuya_magic_spell_message(mock_device_meian_button) }) --- test.socket.zigbee:__expect_send( --- { --- mock_device_meian_button.id, --- PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device_meian_button) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) +test.register_coroutine_test( + "Health check should check all relevant attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added"}) + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send({ + mock_device_meian_button.id, + { + capability_id = "button", component_id = "main", + attribute_id = "button", state = { value = "pushed" } + } + }) + test.socket.zigbee:__expect_send({ mock_device_meian_button.id, tuya_utils.build_tuya_magic_spell_message(mock_device_meian_button) }) + test.socket.zigbee:__expect_send( + { + mock_device_meian_button.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) + } + ) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device_meian_button) + test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") + end + } +) test.register_coroutine_test( "Refresh necessary attributes", diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua index 431cbc2c48..cd50c82ede 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua @@ -40,7 +40,9 @@ 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) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) 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 d6beda0de8..b441c64291 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -39,7 +39,9 @@ local mock_simple_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_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_motion_sensor.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_motion_sensor.lua index c00963c50e..14d1dc504a 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_motion_sensor.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_motion_sensor.lua @@ -39,7 +39,9 @@ local mock_simple_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_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_smoke_detector.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_smoke_detector.lua index cc1ab8fb30..42de1e19f2 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_smoke_detector.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_smoke_detector.lua @@ -39,7 +39,9 @@ local mock_simple_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_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) 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 4da9d411d7..e4070c6f17 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua @@ -95,7 +95,9 @@ local function test_init() test.mock_device.add_test_device(second_mock_child_device) test.mock_device.add_test_device(third_mock_child_device) test.mock_device.add_test_device(fourth_mock_child_device) - test.mock_device.add_test_device(fifth_mock_child_device)end + test.mock_device.add_test_device(fifth_mock_child_device) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) From b56365c81797d39a40083fbb890142e2b220b49e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 15 Jul 2025 11:10:36 -0700 Subject: [PATCH 039/449] Revert "Aeotec Home Energy Meter Gen8 (#1981)" This reverts commit 1b1e49e00e0fc190b852f4521b31d6803e06ace2. --- .../zwave-electric-meter/fingerprints.yml | 16 - ...tec-home-energy-meter-gen8-1-phase-con.yml | 105 --- ...tec-home-energy-meter-gen8-1-phase-pro.yml | 22 - ...tec-home-energy-meter-gen8-2-phase-con.yml | 146 ---- ...tec-home-energy-meter-gen8-2-phase-pro.yml | 31 - ...tec-home-energy-meter-gen8-3-phase-con.yml | 187 ----- ...tec-home-energy-meter-gen8-3-phase-pro.yml | 40 - ...aeotec-home-energy-meter-gen8-sald-con.yml | 15 - ...aeotec-home-energy-meter-gen8-sald-pro.yml | 13 - .../1-phase/init.lua | 199 ----- .../2-phase/init.lua | 195 ----- .../3-phase/init.lua | 199 ----- .../aeotec-home-energy-meter-gen8/init.lua | 50 -- .../zwave-electric-meter/src/init.lua | 24 +- .../zwave-electric-meter/src/preferences.lua | 95 --- ..._aeotec_home_energy_meter_gen8_1_phase.lua | 568 ------------- ..._aeotec_home_energy_meter_gen8_2_phase.lua | 664 --------------- ..._aeotec_home_energy_meter_gen8_3_phase.lua | 760 ------------------ 18 files changed, 1 insertion(+), 3328 deletions(-) delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-con.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-pro.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-con.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-pro.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-con.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-pro.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-con.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-pro.yml delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/1-phase/init.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/2-phase/init.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/3-phase/init.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/init.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/preferences.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_1_phase.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_2_phase.lua delete mode 100644 drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_3_phase.lua diff --git a/drivers/SmartThings/zwave-electric-meter/fingerprints.yml b/drivers/SmartThings/zwave-electric-meter/fingerprints.yml index c580510daa..7c32d646e3 100644 --- a/drivers/SmartThings/zwave-electric-meter/fingerprints.yml +++ b/drivers/SmartThings/zwave-electric-meter/fingerprints.yml @@ -35,22 +35,6 @@ zwaveManufacturer: productType: 0x0002 productId: 0x0001 deviceProfileName: base-electric-meter - - id: "0x0371/0x0003/0x0033" #HEM Gen8 1 Phase EU, AU - deviceLabel: Aeotec Home Energy Meter Gen8 Consumption - manufacturerId: 0x0371 - productId: 0x0033 - deviceProfileName: aeotec-home-energy-meter-gen8-1-phase-con - - id: "0x0371/0x0003/0x0034" # HEM Gen8 3 Phase EU, AU - deviceLabel: Aeotec Home Energy Meter Gen8 Consumption - manufacturerId: 0x0371 - productId: 0x0034 - deviceProfileName: aeotec-home-energy-meter-gen8-3-phase-con - - id: "0x0371/0x0103/0x002E" # HEM Gen8 2 Phase US - deviceLabel: Aeotec Home Energy Meter Gen8 Consumption - manufacturerId: 0x0371 - productType: 0x0103 - productId: 0x002E - deviceProfileName: aeotec-home-energy-meter-gen8-2-phase-con zwaveGeneric: - id: "GenericEnergyMeter" deviceLabel: Energy Monitor diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-con.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-con.yml deleted file mode 100644 index 30dd77c674..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-con.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: aeotec-home-energy-meter-gen8-1-phase-con -components: -- id: main - label: "Sum Consumption" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp1 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -preferences: - - name: thresholdCheck - title: "3. Threshold Check Enable/Disable" - description: "Enable selective reporting only when power change reaches a certain threshold or percentage set in 4 -19 below. This is used to reduce network traffic." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 1 - - name: imWThresholdTotal - title: "4. Import W threshold (total)" - description: "Threshold change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWThresholdPhaseA - title: "5. Import W threshold (Phase A)" - description: "Threshold change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdTotal - title: "8. Export W threshold (total)" - description: "Threshold change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseA - title: "9. Export W threshold (Phase A)" - description: "Threshold change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWPctThresholdTotal - title: "12. Import W threshold (total)" - description: "Percentage change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseA - title: "13. Import W threshold (Phase A)" - description: "Percentage change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdTotal - title: "16. Export W threshold (total)" - description: "Percentage change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseA - title: "17. Export W threshold (Phase A)" - description: "Percentage change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: autoRootDeviceReport - title: "32. Auto report of root device" - description: "Enable automatic report of root device." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 0 diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-pro.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-pro.yml deleted file mode 100644 index 79b1eef8bc..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-1-phase-pro.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: aeotec-home-energy-meter-gen8-1-phase-pro -components: -- id: main - label: "Sum Production" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp2 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-con.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-con.yml deleted file mode 100644 index d0e5d86aba..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-con.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: aeotec-home-energy-meter-gen8-2-phase-con -components: -- id: main - label: "Sum Consumption" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp1 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp3 - label: "Clamp 2" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -preferences: - - name: thresholdCheck - title: "3. Threshold Check Enable/Disable" - description: "Enable selective reporting only when power change reaches a certain threshold or percentage set in 4 -19 below. This is used to reduce network traffic." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 1 - - name: imWThresholdTotal - title: "4. Import W threshold (total)" - description: "Threshold change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWThresholdPhaseA - title: "5. Import W threshold (Phase A)" - description: "Threshold change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWhresholdPhaseB - title: "6. Import W threshold (Phase B)" - description: "Threshold change in import wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWhresholdTotal - title: "8. Export W threshold (total)" - description: "Threshold change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseA - title: "9. Export W threshold (Phase A)" - description: "Threshold change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseB - title: "10. Export W threshold (Phase B)" - description: "Threshold change in export wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWPctThresholdTotal - title: "12. Import W threshold (total)" - description: "Percentage change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseA - title: "13. Import W threshold (Phase A)" - description: "Percentage change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseB - title: "14. Import W threshold (Phase B)." - description: "Percentage change in import wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdTotal - title: "16. Export W threshold (total)" - description: "Percentage change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseA - title: "17. Export W threshold (Phase A)" - description: "Percentage change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseB - title: "18. Export W threshold (Phase B)" - description: "Percentage change in export wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: autoRootDeviceReport - title: "32. Auto report of root device" - description: "Enable automatic report of root device." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 0 diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-pro.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-pro.yml deleted file mode 100644 index 717a91aeef..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-2-phase-pro.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: aeotec-home-energy-meter-gen8-2-phase-pro -components: -- id: main - label: "Sum Production" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp2 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp4 - label: "Clamp 2" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-con.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-con.yml deleted file mode 100644 index 80c19f91a7..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-con.yml +++ /dev/null @@ -1,187 +0,0 @@ -name: aeotec-home-energy-meter-gen8-3-phase-con -components: -- id: main - label: "Sum Consumption" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp1 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp3 - label: "Clamp 2" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp5 - label: "Clamp 3" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -preferences: - - name: thresholdCheck - title: "3. Threshold Check Enable/Disable" - description: "Enable selective reporting only when power change reaches a certain threshold or percentage set in 4 -19 below. This is used to reduce network traffic." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 1 - - name: imWThresholdTotal - title: "4. Import W threshold (total)" - description: "Threshold change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWThresholdPhaseA - title: "5. Import W threshold (Phase A)" - description: "Threshold change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWhresholdPhaseB - title: "6. Import W threshold (Phase B)" - description: "Threshold change in import wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imtWThresholdPhaseC - title: "7. Import W threshold (Phase C)" - description: "Threshold change in import wattage to induce an automatic report (Phase C)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWhresholdTotal - title: "8. Export W threshold (total)" - description: "Threshold change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseA - title: "9. Export W threshold (Phase A)" - description: "Threshold change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseB - title: "10. Export W threshold (Phase B)" - description: "Threshold change in export wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: exWThresholdPhaseC - title: "11. Export W threshold (Phase C)" - description: "Threshold change in export wattage to induce an automatic report (Phase C)." - preferenceType: integer - definition: - minimum: 0 - maximum: 60000 - default: 50 - - name: imWPctThresholdTotal - title: "12. Import W threshold (total)" - description: "Percentage change in import wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseA - title: "13. Import W threshold (Phase A)" - description: "Percentage change in import wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseB - title: "14. Import W threshold (Phase B)" - description: "Percentage change in import wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: imWPctThresholdPhaseC - title: "15. Import W threshold (Phase C)" - description: "Percentage change in import wattage to induce an automatic report (Phase C)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdTotal - title: "16. Export W threshold (total)" - description: "Percentage change in export wattage to induce an automatic report (Whole HEM)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseA - title: "17. Export W threshold (Phase A)" - description: "Percentage change in export wattage to induce an automatic report (Phase A)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseB - title: "18. Export W threshold (Phase B)" - description: "Percentage change in export wattage to induce an automatic report (Phase B)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: exWPctThresholdPhaseC - title: "19. Export W threshold (Phase C)" - description: "Percentage change in export wattage to induce an automatic report (Phase C)." - preferenceType: integer - definition: - minimum: 0 - maximum: 100 - default: 20 - - name: autoRootDeviceReport - title: "32. Auto report of root device" - description: "Enable automatic report of root device." - preferenceType: enumeration - definition: - options: - 0: "Disable" - 1: "Enable" - default: 0 diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-pro.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-pro.yml deleted file mode 100644 index e877276a7b..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-3-phase-pro.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: aeotec-home-energy-meter-gen8-3-phase-pro -components: -- id: main - label: "Sum Production" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp2 - label: "Clamp 1" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp4 - label: "Clamp 2" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter -- id: clamp6 - label: "Clamp 3" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - categories: - - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-con.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-con.yml deleted file mode 100644 index 9b78fca82a..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-con.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: aeotec-home-energy-meter-gen8-sald-con -components: -- id: main - label: "Settled Consumption" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - - id: powerConsumptionReport - version: 1 - categories: - - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-pro.yml b/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-pro.yml deleted file mode 100644 index 1ff680ec76..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/profiles/aeotec-home-energy-meter-gen8-sald-pro.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: aeotec-home-energy-meter-gen8-sald-pro -components: -- id: main - label: "Settled Production" - capabilities: - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: refresh - version: 1 - categories: - - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/1-phase/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/1-phase/init.lua deleted file mode 100644 index 05be152d65..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/1-phase/init.lua +++ /dev/null @@ -1,199 +0,0 @@ --- Copyright 2025 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 st_device = require "st.device" -local capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) ---- @type st.zwave.CommandClass.Meter -local Meter = (require "st.zwave.CommandClass.Meter")({ version=4 }) ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" -local utils = require "st.utils" - -local AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS = { - { mfr = 0x0371, prod = 0x0003, model = 0x0033 }, -- HEM Gen8 1 Phase EU - { mfr = 0x0371, prod = 0x0102, model = 0x002E } -- HEM Gen8 1 Phase AU -} - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" -local POWER_UNIT_WATT = "W" -local ENERGY_UNIT_KWH = "kWh" - -local HEM8_DEVICES = { - { profile = 'aeotec-home-energy-meter-gen8-1-phase-con', name = 'Aeotec Home Energy Meter 8 Consumption', endpoints = { 1, 3 } }, - { profile = 'aeotec-home-energy-meter-gen8-1-phase-pro', name = 'Aeotec Home Energy Meter 8 Production', child_key = 'pro', endpoints = { 2, 4 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-con', name = 'Aeotec Home Energy Meter 8 Settled Consumption', child_key = 'sald-con', endpoints = { 5 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-pro', name = 'Aeotec Home Energy Meter 8 Settled Production', child_key = 'sald-pro', endpoints = { 6 } } -} - -local function can_handle_aeotec_meter_gen8_1_phase(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - -local function find_hem8_child_device_key_by_endpoint(endpoint) - for _, child in ipairs(HEM8_DEVICES) do - if child.endpoints then - for _, e in ipairs(child.endpoints) do - if e == endpoint then - return child.child_key - end - end - end - end -end - -local function emit_power_consumption_report_event(device, value, channel) - -- powerConsumptionReport report interval - local current_time = os.time() - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local next_time = last_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports - if current_time < next_time then - return - end - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - local raw_value = value.value * 1000 -- 'Wh' - - local delta_energy = 0.0 - 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 - device:emit_event_for_endpoint(channel, capabilities.powerConsumptionReport.powerConsumption({ - energy = raw_value, - deltaEnergy = delta_energy - })) -end - -local function meter_report_handler(driver, device, cmd, zb_rx) - local endpoint = cmd.src_channel - local device_to_emit_with = device - local child_device_key = find_hem8_child_device_key_by_endpoint(endpoint); - local child_device = device:get_child_by_parent_assigned_key(child_device_key) - - if(child_device) then - device_to_emit_with = child_device - end - - if cmd.args.scale == Meter.scale.electric_meter.KILOWATT_HOURS then - local event_arguments = { - value = cmd.args.meter_value, - unit = ENERGY_UNIT_KWH - } - -- energyMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.energyMeter.energy(event_arguments) - ) - - if endpoint == 5 then - -- powerConsumptionReport - emit_power_consumption_report_event(device_to_emit_with, { value = event_arguments.value }, endpoint) - end - elseif cmd.args.scale == Meter.scale.electric_meter.WATTS then - local event_arguments = { - value = cmd.args.meter_value, - unit = POWER_UNIT_WATT - } - -- powerMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.powerMeter.power(event_arguments) - ) - end -end - -local function do_refresh(self, device) - for _, d in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(d.endpoints) do - device:send(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, {dst_channels = {endpoint}})) - device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS}, {dst_channels = {endpoint}})) - end - end -end - -local function component_to_endpoint(device, component_id) - local ep_num = component_id:match("clamp(%d)") - return { ep_num and tonumber(ep_num) } -end - -local function endpoint_to_component(device, ep) - local meter_comp = string.format("clamp%d", ep) - if device.profile.components[meter_comp] ~= nil then - return meter_comp - else - return "main" - 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 function device_added(driver, device) - if device.network_type == st_device.NETWORK_TYPE_ZWAVE and not (device.child_ids and utils.table_size(device.child_ids) ~= 0) then - for i, hem8_child in ipairs(HEM8_DEVICES) do - if(hem8_child["child_key"]) then - local name = hem8_child.name - local metadata = { - type = "EDGE_CHILD", - label = name, - profile = hem8_child.profile, - parent_device_id = device.id, - parent_assigned_child_key = hem8_child.child_key, - vendor_provided_label = name - } - driver:try_create_device(metadata) - end - end - end - do_refresh(driver, device) -end - -local do_configure = function (self, device) - device:send(Configuration:Set({parameter_number = 111, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 112, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 113, configuration_value = 300, size = 4})) -- ...every 5 min -end - -local aeotec_home_energy_meter_gen8_1_phase = { - NAME = "Aeotec Home Energy Meter Gen8", - supported_capabilities = { - capabilities.powerConsumptionReport - }, - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh - } - }, - zwave_handlers = { - [cc.METER] = { - [Meter.REPORT] = meter_report_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure, - added = device_added, - init = device_init - }, - can_handle = can_handle_aeotec_meter_gen8_1_phase -} - -return aeotec_home_energy_meter_gen8_1_phase diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/2-phase/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/2-phase/init.lua deleted file mode 100644 index 00770b6a75..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/2-phase/init.lua +++ /dev/null @@ -1,195 +0,0 @@ --- Copyright 2025 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 st_device = require "st.device" -local capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) ---- @type st.zwave.CommandClass.Meter -local Meter = (require "st.zwave.CommandClass.Meter")({ version=4 }) ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" -local utils = require "st.utils" - -local AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS = { - { mfr = 0x0371, prod = 0x0103, model = 0x002E } -- HEM Gen8 2 Phase US -} - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" -local POWER_UNIT_WATT = "W" -local ENERGY_UNIT_KWH = "kWh" - -local HEM8_DEVICES = { - { profile = 'aeotec-home-energy-meter-gen8-3-phase-con', name = 'Aeotec Home Energy Meter 8 Consumption', endpoints = { 1, 3, 5 } }, - { profile = 'aeotec-home-energy-meter-gen8-2-phase-pro', name = 'Aeotec Home Energy Meter 8 Production', child_key = 'pro', endpoints = { 2, 4, 6 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-con', name = 'Aeotec Home Energy Meter 8 Settled Consumption', child_key = 'sald-con', endpoints = { 7 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-pro', name = 'Aeotec Home Energy Meter 8 Settled Production', child_key = 'sald-pro', endpoints = { 8 } } -} - -local function can_handle_aeotec_meter_gen8_2_phase(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - -local function find_hem8_child_device_key_by_endpoint(endpoint) - for _, child in ipairs(HEM8_DEVICES) do - if child.endpoints then - for _, e in ipairs(child.endpoints) do - if e == endpoint then - return child.child_key - end - end - end - end -end - -local function emit_power_consumption_report_event(device, value, channel) - -- powerConsumptionReport report interval - local current_time = os.time() - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local next_time = last_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports - if current_time < next_time then - return - end - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - local raw_value = value.value * 1000 -- 'Wh' - - local delta_energy = 0.0 - 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 - device:emit_event_for_endpoint(channel, capabilities.powerConsumptionReport.powerConsumption({ - energy = raw_value, - deltaEnergy = delta_energy - })) -end - -local function meter_report_handler(driver, device, cmd, zb_rx) - local endpoint = cmd.src_channel - local device_to_emit_with = device - local child_device_key = find_hem8_child_device_key_by_endpoint(endpoint); - local child_device = device:get_child_by_parent_assigned_key(child_device_key) - - if(child_device) then - device_to_emit_with = child_device - end - - if cmd.args.scale == Meter.scale.electric_meter.KILOWATT_HOURS then - local event_arguments = { - value = cmd.args.meter_value, - unit = ENERGY_UNIT_KWH - } - -- energyMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.energyMeter.energy(event_arguments) - ) - - if endpoint == 7 then - -- powerConsumptionReport - emit_power_consumption_report_event(device_to_emit_with, { value = event_arguments.value }, endpoint) - end - elseif cmd.args.scale == Meter.scale.electric_meter.WATTS then - local event_arguments = { - value = cmd.args.meter_value, - unit = POWER_UNIT_WATT - } - -- powerMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.powerMeter.power(event_arguments) - ) - end -end - -local function do_refresh(self, device) - for _, d in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(d.endpoints) do - device:send(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, {dst_channels = {endpoint}})) - device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS}, {dst_channels = {endpoint}})) - end - end -end - -local function component_to_endpoint(device, component_id) - local ep_num = component_id:match("clamp(%d)") - return { ep_num and tonumber(ep_num) } -end -local function endpoint_to_component(device, ep) - local meter_comp = string.format("clamp%d", ep) - if device.profile.components[meter_comp] ~= nil then - return meter_comp - else - return "main" - 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 function device_added(driver, device) - if device.network_type == st_device.NETWORK_TYPE_ZWAVE and not (device.child_ids and utils.table_size(device.child_ids) ~= 0) then - for i, hem8_child in ipairs(HEM8_DEVICES) do - if(hem8_child["child_key"]) then - local name = hem8_child.name - local metadata = { - type = "EDGE_CHILD", - label = name, - profile = hem8_child.profile, - parent_device_id = device.id, - parent_assigned_child_key = hem8_child.child_key, - vendor_provided_label = name - } - driver:try_create_device(metadata) - end - end - end - do_refresh(driver, device) -end - -local do_configure = function (self, device) - device:send(Configuration:Set({parameter_number = 111, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 112, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 113, configuration_value = 300, size = 4})) -- ...every 5 min -end - -local aeotec_home_energy_meter_gen8_2_phase = { - NAME = "Aeotec Home Energy Meter Gen8", - supported_capabilities = { - capabilities.powerConsumptionReport - }, - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh - } - }, - zwave_handlers = { - [cc.METER] = { - [Meter.REPORT] = meter_report_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure, - added = device_added, - init = device_init - }, - can_handle = can_handle_aeotec_meter_gen8_2_phase -} - -return aeotec_home_energy_meter_gen8_2_phase diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/3-phase/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/3-phase/init.lua deleted file mode 100644 index c7a5c6884e..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/3-phase/init.lua +++ /dev/null @@ -1,199 +0,0 @@ --- Copyright 2025 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 st_device = require "st.device" -local capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) ---- @type st.zwave.CommandClass.Meter -local Meter = (require "st.zwave.CommandClass.Meter")({ version=4 }) ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" -local utils = require "st.utils" - -local AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS = { - { mfr = 0x0371, prod = 0x0003, model = 0x0034 }, -- HEM Gen8 3 Phase EU - { mfr = 0x0371, prod = 0x0102, model = 0x0034 } -- HEM Gen8 3 Phase AU -} - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" -local POWER_UNIT_WATT = "W" -local ENERGY_UNIT_KWH = "kWh" - -local HEM8_DEVICES = { - { profile = 'aeotec-home-energy-meter-gen8-3-phase-con', name = 'Aeotec Home Energy Meter 8 Consumption', endpoints = { 1, 3, 5, 7 } }, - { profile = 'aeotec-home-energy-meter-gen8-3-phase-pro', name = 'Aeotec Home Energy Meter 8 Production', child_key = 'pro', endpoints = { 2, 4, 6, 8 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-con', name = 'Aeotec Home Energy Meter 8 Settled Consumption', child_key = 'sald-con', endpoints = { 9 } }, - { profile = 'aeotec-home-energy-meter-gen8-sald-pro', name = 'Aeotec Home Energy Meter 8 Settled Production', child_key = 'sald-pro', endpoints = { 10 } } -} - -local function can_handle_aeotec_meter_gen8_3_phase(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - -local function find_hem8_child_device_key_by_endpoint(endpoint) - for _, child in ipairs(HEM8_DEVICES) do - if child.endpoints then - for _, e in ipairs(child.endpoints) do - if e == endpoint then - return child.child_key - end - end - end - end -end - -local function emit_power_consumption_report_event(device, value, channel) - -- powerConsumptionReport report interval - local current_time = os.time() - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local next_time = last_time + 60 * 15 -- 15 mins, the minimum interval allowed between reports - if current_time < next_time then - return - end - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - local raw_value = value.value * 1000 -- 'Wh' - - local delta_energy = 0.0 - 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 - device:emit_event_for_endpoint(channel, capabilities.powerConsumptionReport.powerConsumption({ - energy = raw_value, - deltaEnergy = delta_energy - })) -end - -local function meter_report_handler(driver, device, cmd, zb_rx) - local endpoint = cmd.src_channel - local device_to_emit_with = device - local child_device_key = find_hem8_child_device_key_by_endpoint(endpoint); - local child_device = device:get_child_by_parent_assigned_key(child_device_key) - - if(child_device) then - device_to_emit_with = child_device - end - - if cmd.args.scale == Meter.scale.electric_meter.KILOWATT_HOURS then - local event_arguments = { - value = cmd.args.meter_value, - unit = ENERGY_UNIT_KWH - } - -- energyMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.energyMeter.energy(event_arguments) - ) - - if endpoint == 9 then - -- powerConsumptionReport - emit_power_consumption_report_event(device_to_emit_with, { value = event_arguments.value }, endpoint) - end - elseif cmd.args.scale == Meter.scale.electric_meter.WATTS then - local event_arguments = { - value = cmd.args.meter_value, - unit = POWER_UNIT_WATT - } - -- powerMeter - device_to_emit_with:emit_event_for_endpoint( - cmd.src_channel, - capabilities.powerMeter.power(event_arguments) - ) - end -end - -local function do_refresh(self, device) - for _, d in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(d.endpoints) do - device:send(Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, {dst_channels = {endpoint}})) - device:send(Meter:Get({scale = Meter.scale.electric_meter.WATTS}, {dst_channels = {endpoint}})) - end - end -end - -local function component_to_endpoint(device, component_id) - local ep_num = component_id:match("clamp(%d)") - return { ep_num and tonumber(ep_num) } -end - -local function endpoint_to_component(device, ep) - local meter_comp = string.format("clamp%d", ep) - if device.profile.components[meter_comp] ~= nil then - return meter_comp - else - return "main" - 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 function device_added(driver, device) - if device.network_type == st_device.NETWORK_TYPE_ZWAVE and not (device.child_ids and utils.table_size(device.child_ids) ~= 0) then - for i, hem8_child in ipairs(HEM8_DEVICES) do - if(hem8_child["child_key"]) then - local name = hem8_child.name - local metadata = { - type = "EDGE_CHILD", - label = name, - profile = hem8_child.profile, - parent_device_id = device.id, - parent_assigned_child_key = hem8_child.child_key, - vendor_provided_label = name - } - driver:try_create_device(metadata) - end - end - end - do_refresh(driver, device) -end - -local do_configure = function (self, device) - device:send(Configuration:Set({parameter_number = 111, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 112, configuration_value = 300, size = 4})) -- ...every 5 min - device:send(Configuration:Set({parameter_number = 113, configuration_value = 300, size = 4})) -- ...every 5 min -end - -local aeotec_home_energy_meter_gen8_3_phase = { - NAME = "Aeotec Home Energy Meter Gen8", - supported_capabilities = { - capabilities.powerConsumptionReport - }, - capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh - } - }, - zwave_handlers = { - [cc.METER] = { - [Meter.REPORT] = meter_report_handler - } - }, - lifecycle_handlers = { - doConfigure = do_configure, - added = device_added, - init = device_init - }, - can_handle = can_handle_aeotec_meter_gen8_3_phase -} - -return aeotec_home_energy_meter_gen8_3_phase diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/init.lua deleted file mode 100644 index 767f0d7d8f..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-home-energy-meter-gen8/init.lua +++ /dev/null @@ -1,50 +0,0 @@ --- Copyright 2025 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 AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS = { - { mfr = 0x0371, prod = 0x0003, model = 0x0033 }, -- HEM Gen8 1 Phase EU - { mfr = 0x0371, prod = 0x0003, model = 0x0034 }, -- HEM Gen8 3 Phase EU - { mfr = 0x0371, prod = 0x0103, model = 0x002E }, -- HEM Gen8 2 Phase US - { mfr = 0x0371, prod = 0x0102, model = 0x002E }, -- HEM Gen8 1 Phase AU - { mfr = 0x0371, prod = 0x0102, model = 0x0034 }, -- HEM Gen8 3 Phase AU -} - -local function can_handle_aeotec_meter_gen8(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_HOME_ENERGY_METER_GEN8_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("aeotec-home-energy-meter-gen8") - return true, subdriver - end - end - return false -end - -local function device_added(driver, device) - device:refresh() -end - -local aeotec_home_energy_meter_gen8 = { - NAME = "Aeotec Home Energy Meter Gen8", - lifecycle_handlers = { - added = device_added - }, - can_handle = can_handle_aeotec_meter_gen8, - sub_drivers = { - require("aeotec-home-energy-meter-gen8/1-phase"), - require("aeotec-home-energy-meter-gen8/2-phase"), - require("aeotec-home-energy-meter-gen8/3-phase") - } -} - -return aeotec_home_energy_meter_gen8 diff --git a/drivers/SmartThings/zwave-electric-meter/src/init.lua b/drivers/SmartThings/zwave-electric-meter/src/init.lua index e751c133e9..2ef9f20281 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/init.lua @@ -17,31 +17,11 @@ local capabilities = require "st.capabilities" local defaults = require "st.zwave.defaults" --- @type st.zwave.Driver local ZwaveDriver = require "st.zwave.driver" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1}) - -local preferencesMap = require "preferences" local device_added = function (self, device) device:refresh() end ---- Handle preference changes ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param event table ---- @param args -local function info_changed(driver, device, event, args) - local preferences = preferencesMap.get_device_parameters(device) - for id, value in pairs(device.preferences) do - if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then - local new_parameter_value = preferencesMap.to_numeric_value(device.preferences[id]) - device:send(Configuration:Set({ parameter_number = preferences[id].parameter_number, size = preferences[id].size, configuration_value = new_parameter_value })) - end - end -end - local driver_template = { supported_capabilities = { capabilities.powerMeter, @@ -49,14 +29,12 @@ local driver_template = { capabilities.refresh }, lifecycle_handlers = { - infoChanged = info_changed, added = device_added }, sub_drivers = { require("qubino-meter"), require("aeotec-gen5-meter"), - require("aeon-meter"), - require("aeotec-home-energy-meter-gen8") + require("aeon-meter") } } diff --git a/drivers/SmartThings/zwave-electric-meter/src/preferences.lua b/drivers/SmartThings/zwave-electric-meter/src/preferences.lua deleted file mode 100644 index 172b273e7d..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/preferences.lua +++ /dev/null @@ -1,95 +0,0 @@ -local devices = { - AEOTEC_HOME_ENERGY_METER_GEN8_1_PHASE = { - MATCHING_MATRIX = { - mfrs = 0x0371, - product_types = {0x0003, 0x0102 }, - product_ids = 0x0033 - }, - PARAMETERS = { - thresholdCheck = {parameter_number = 3, size = 1}, - imWThresholdTotal = {parameter_number = 4, size = 2}, - imWThresholdPhaseA = {parameter_number = 5, size = 2}, - exWThresholdTotal = {parameter_number = 8, size = 2}, - exWThresholdPhaseA = {parameter_number = 9, size = 2}, - imtWPctThresholdTotal = {parameter_number = 12, size = 1}, - imWPctThresholdPhaseA = {parameter_number = 13, size = 1}, - exWPctThresholdTotal = {parameter_number = 16, size = 1}, - exWPctThresholdPhaseA = {parameter_number = 17, size = 1}, - autoRootDeviceReport = {parameter_number = 32, size = 1}, - } - }, - AEOTEC_HOME_ENERGY_METER_GEN8_2_PHASE = { - MATCHING_MATRIX = { - mfrs = 0x0371, - product_types = 0x0103, - product_ids = 0x002E - }, - PARAMETERS = { - thresholdCheck = {parameter_number = 3, size = 1}, - imWThresholdTotal = {parameter_number = 4, size = 2}, - imWThresholdPhaseA = {parameter_number = 5, size = 2}, - imWThresholdPhaseB = {parameter_number = 6, size = 2}, - exWThresholdTotal = {parameter_number = 8, size = 2}, - exWThresholdPhaseA = {parameter_number = 9, size = 2}, - exWThresholdPhaseB = {parameter_number = 10, size = 2}, - imtWPctThresholdTotal = {parameter_number = 12, size = 1}, - imWPctThresholdPhaseA = {parameter_number = 13, size = 1}, - imWPctThresholdPhaseB = {parameter_number = 14, size = 1}, - exWPctThresholdTotal = {parameter_number = 16, size = 1}, - exWPctThresholdPhaseA = {parameter_number = 17, size = 1}, - exWPctThresholdPhaseB = {parameter_number = 18, size = 1}, - autoRootDeviceReport = {parameter_number = 32, size = 1}, - } - }, - AEOTEC_HOME_ENERGY_METER_GEN8_3_PHASE = { - MATCHING_MATRIX = { - mfrs = 0x0371, - product_types = {0x0003, 0x0102}, - product_ids = 0x0034 - }, - PARAMETERS = { - thresholdCheck = {parameter_number = 3, size = 1}, - imWThresholdTotal = {parameter_number = 4, size = 2}, - imWThresholdPhaseA = {parameter_number = 5, size = 2}, - imWThresholdPhaseB = {parameter_number = 6, size = 2}, - imWThresholdPhaseC = {parameter_number = 7, size = 2}, - exWThresholdTotal = {parameter_number = 8, size = 2}, - exWThresholdPhaseA = {parameter_number = 9, size = 2}, - exWThresholdPhaseB = {parameter_number = 10, size = 2}, - exWThresholdPhaseC = {parameter_number = 11, size = 2}, - imtWPctThresholdTotal = {parameter_number = 12, size = 1}, - imWPctThresholdPhaseA = {parameter_number = 13, size = 1}, - imWPctThresholdPhaseB = {parameter_number = 14, size = 1}, - imWPctThresholdPhaseC = {parameter_number = 15, size = 1}, - exWPctThresholdTotal = {parameter_number = 16, size = 1}, - exWPctThresholdPhaseA = {parameter_number = 17, size = 1}, - exWPctThresholdPhaseB = {parameter_number = 18, size = 1}, - exWPctThresholdPhaseC = {parameter_number = 19, size = 1}, - autoRootDeviceReport = {parameter_number = 32, size = 1}, - } - } -} - -local preferences = {} - -preferences.get_device_parameters = function(zw_device) - for _, device in pairs(devices) do - if zw_device:id_match( - device.MATCHING_MATRIX.mfrs, - device.MATCHING_MATRIX.product_types, - device.MATCHING_MATRIX.product_ids) then - return device.PARAMETERS - end - end - return nil -end - -preferences.to_numeric_value = function(new_value) - local numeric = tonumber(new_value) - if numeric == nil then -- in case the value is boolean - numeric = new_value and 1 or 0 - end - return numeric -end - -return preferences \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_1_phase.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_1_phase.lua deleted file mode 100644 index 3878a11eb0..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_1_phase.lua +++ /dev/null @@ -1,568 +0,0 @@ --- Copyright 2025 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 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 Meter = (require "st.zwave.CommandClass.Meter")({version=4}) -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) -local t_utils = require "integration_test.utils" - -local AEOTEC_MFR_ID = 0x0371 -local AEOTEC_METER_PROD_TYPE = 0x0003 -local AEOTEC_METER_PROD_ID = 0x0033 - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" - -local aeotec_meter_endpoints = { - { - command_classes = { - {value = zw.METER} - } - } -} - -local HEM8_DEVICES = { - { - profile = 'aeotec-home-energy-meter-gen8-1-phase-con', - name = 'Aeotec Home Energy Meter 8 Consumption', - endpoints = { 1, 3 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-1-phase-pro', - name = 'Aeotec Home Energy Meter 8 Production', - child_key = 'pro', - endpoints = { 2, 4 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-con', - name = 'Aeotec Home Energy Meter 8 Settled Consumption', - child_key = 'sald-con', - endpoints = { 5 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-pro', - name = 'Aeotec Home Energy Meter 8 Settled Production', - child_key = 'sald-pro', - endpoints = { 6 } - } -} - -local mock_parent = test.mock_device.build_test_zwave_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[1].profile .. '.yml'), - zwave_endpoints = aeotec_meter_endpoints, - zwave_manufacturer_id = AEOTEC_MFR_ID, - zwave_product_type = AEOTEC_METER_PROD_TYPE, - zwave_product_id = AEOTEC_METER_PROD_ID -}) - -local mock_child_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[2].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[2].child_key -}) - -local mock_child_sald_con = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[3].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[3].child_key -}) - -local mock_child_sald_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[4].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[4].child_key -}) - -local function test_init() - test.mock_device.add_test_device(mock_parent) - test.mock_device.add_test_device(mock_child_prod) - test.mock_device.add_test_device(mock_child_sald_con) - test.mock_device.add_test_device(mock_child_sald_prod) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Added lifecycle event should create children for parent device", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "added" }) - - for _, child in ipairs(HEM8_DEVICES) do - if(child["child_key"]) then - mock_parent:expect_device_create( - { - type = "EDGE_CHILD", - label = child.name, - profile = child.profile, - parent_device_id = mock_parent.id, - parent_assigned_child_key = child.child_key - } - ) - end - end - -- Refresh - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - end -) - -test.register_coroutine_test( - "Configure should configure all necessary attributes", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "doConfigure" }) - - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 111, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 112, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 113, size = 4, configuration_value = 300}) - )) - mock_parent:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Power meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 3 and endpoint ~= 4 and endpoint ~= 5 and endpoint ~= 6 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 27 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Energy meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 3 and endpoint ~= 4 and endpoint ~= 5 and endpoint ~= 6 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Report consumption and power consumption report after 15 minutes", function() - -- set time to trigger power consumption report - local current_time = os.time() - 60 * 20 - mock_child_sald_con:set_field(LAST_REPORT_TIME, current_time) - - test.socket.zwave:__queue_receive( - { - mock_child_sald_con.id, - zw_test_utils.zwave_test_build_receive_command(Meter:Report( - { - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, - { - encap = zw.ENCAP.AUTO, - src_channel = 5, - dst_channels = {0} - } - )) - } - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ deltaEnergy = 0.0, energy = 5000 })) - ) - end -) - -test.register_coroutine_test( - "Handle preference: thresholdCheck (parameter 3) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - thresholdCheck = 0 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 3, - configuration_value = 0, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdTotal (parameter 4) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 4, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseA (parameter 5) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 5, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdTotal (parameter 8) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 8, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseA (parameter 9) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 9, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imtWPctThresholdTotal (parameter 12) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imtWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 12, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseA (parameter 13) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 13, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdTotal (parameter 16) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 16, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdPhaseA (parameter 17) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 17, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: autoRootDeviceReport (parameter 32) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - autoRootDeviceReport = 1 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 32, - configuration_value = 1, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Refresh sends commands to all components including base device", - function() - -- refresh commands for zwave devices do not have guaranteed ordering - test.socket.zwave:__set_channel_ordering("relaxed") - - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - - test.socket.capability:__queue_receive({ - mock_parent.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - }) - end -) - -test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_2_phase.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_2_phase.lua deleted file mode 100644 index b30b8d3a90..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_2_phase.lua +++ /dev/null @@ -1,664 +0,0 @@ --- Copyright 2025 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 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 Meter = (require "st.zwave.CommandClass.Meter")({version=4}) -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) -local t_utils = require "integration_test.utils" - -local AEOTEC_MFR_ID = 0x0371 -local AEOTEC_METER_PROD_TYPE = 0x0103 -local AEOTEC_METER_PROD_ID = 0x002E - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" - -local aeotec_meter_endpoints = { - { - command_classes = { - {value = zw.METER} - } - } -} - -local HEM8_DEVICES = { - { - profile = 'aeotec-home-energy-meter-gen8-2-phase-con', - name = 'Aeotec Home Energy Meter 8 Consumption', - endpoints = { 1, 3, 5 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-2-phase-pro', - name = 'Aeotec Home Energy Meter 8 Production', - child_key = 'pro', - endpoints = { 2, 4, 6 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-con', - name = 'Aeotec Home Energy Meter 8 Settled Consumption', - child_key = 'sald-con', - endpoints = { 7 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-pro', - name = 'Aeotec Home Energy Meter 8 Settled Production', - child_key = 'sald-pro', - endpoints = { 8 } - } -} - -local mock_parent = test.mock_device.build_test_zwave_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[1].profile .. '.yml'), - zwave_endpoints = aeotec_meter_endpoints, - zwave_manufacturer_id = AEOTEC_MFR_ID, - zwave_product_type = AEOTEC_METER_PROD_TYPE, - zwave_product_id = AEOTEC_METER_PROD_ID -}) - -local mock_child_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[2].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[2].child_key -}) - -local mock_child_sald_con = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[3].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[3].child_key -}) - -local mock_child_sald_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[4].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[4].child_key -}) - -local function test_init() - test.mock_device.add_test_device(mock_parent) - test.mock_device.add_test_device(mock_child_prod) - test.mock_device.add_test_device(mock_child_sald_con) - test.mock_device.add_test_device(mock_child_sald_prod) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Added lifecycle event should create children for parent device", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "added" }) - - for _, child in ipairs(HEM8_DEVICES) do - if(child["child_key"]) then - mock_parent:expect_device_create( - { - type = "EDGE_CHILD", - label = child.name, - profile = child.profile, - parent_device_id = mock_parent.id, - parent_assigned_child_key = child.child_key - } - ) - end - end - -- Refresh - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - end -) - -test.register_coroutine_test( - "Configure should configure all necessary attributes", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "doConfigure" }) - - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 111, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 112, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 113, size = 4, configuration_value = 300}) - )) - mock_parent:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Power meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 5 and endpoint ~= 6 and endpoint ~= 7 and endpoint ~= 8 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 27 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Energy meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 5 and endpoint ~= 6 and endpoint ~= 7 and endpoint ~= 8 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Report consumption and power consumption report after 15 minutes", function() - -- set time to trigger power consumption report - local current_time = os.time() - 60 * 20 - mock_child_sald_con:set_field(LAST_REPORT_TIME, current_time) - - test.socket.zwave:__queue_receive( - { - mock_child_sald_con.id, - zw_test_utils.zwave_test_build_receive_command(Meter:Report( - { - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, - { - encap = zw.ENCAP.AUTO, - src_channel = 7, - dst_channels = {0} - } - )) - } - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ deltaEnergy = 0.0, energy = 5000 })) - ) - end -) - -test.register_coroutine_test( - "Handle preference: thresholdCheck (parameter 3) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - thresholdCheck = 0 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 3, - configuration_value = 0, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdTotal (parameter 4) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 4, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseA (parameter 5) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 5, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseB (parameter 6) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseB = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 6, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdTotal (parameter 8) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 8, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseA (parameter 9) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 9, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseB (parameter 10) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseB = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 10, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imtWPctThresholdTotal (parameter 12) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imtWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 12, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseA (parameter 13) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 13, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseB (parameter 14) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseB = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 14, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdTotal (parameter 16) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 16, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdPhaseA (parameter 17) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 17, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdPhaseB (parameter 18) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseB = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 18, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: autoRootDeviceReport (parameter 32) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - autoRootDeviceReport = 1 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 32, - configuration_value = 1, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Refresh sends commands to all components including base device", - function() - -- refresh commands for zwave devices do not have guaranteed ordering - test.socket.zwave:__set_channel_ordering("relaxed") - - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - - test.socket.capability:__queue_receive({ - mock_parent.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - }) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_3_phase.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_3_phase.lua deleted file mode 100644 index b95de73e50..0000000000 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_home_energy_meter_gen8_3_phase.lua +++ /dev/null @@ -1,760 +0,0 @@ --- Copyright 2025 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 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 Meter = (require "st.zwave.CommandClass.Meter")({version=4}) -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local t_utils = require "integration_test.utils" - -local AEOTEC_MFR_ID = 0x0371 -local AEOTEC_METER_PROD_TYPE = 0x0003 -local AEOTEC_METER_PROD_ID = 0x0034 - -local LAST_REPORT_TIME = "LAST_REPORT_TIME" - -local aeotec_meter_endpoints = { - { - command_classes = { - {value = zw.METER} - } - } -} - -local HEM8_DEVICES = { - { - profile = 'aeotec-home-energy-meter-gen8-3-phase-con', - name = 'Aeotec Home Energy Meter 8 Consumption', - endpoints = { 1, 3, 5, 7 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-3-phase-pro', - name = 'Aeotec Home Energy Meter 8 Production', - child_key = 'pro', - endpoints = { 2, 4, 6, 8 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-con', - name = 'Aeotec Home Energy Meter 8 Settled Consumption', - child_key = 'sald-con', - endpoints = { 9 } - }, - { - profile = 'aeotec-home-energy-meter-gen8-sald-pro', - name = 'Aeotec Home Energy Meter 8 Settled Production', - child_key = 'sald-pro', - endpoints = { 10 } - } -} - -local mock_parent = test.mock_device.build_test_zwave_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[1].profile .. '.yml'), - zwave_endpoints = aeotec_meter_endpoints, - zwave_manufacturer_id = AEOTEC_MFR_ID, - zwave_product_type = AEOTEC_METER_PROD_TYPE, - zwave_product_id = AEOTEC_METER_PROD_ID -}) - -local mock_child_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[2].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[2].child_key -}) - -local mock_child_sald_con = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[3].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[3].child_key -}) - -local mock_child_sald_prod = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition(HEM8_DEVICES[4].profile .. '.yml'), - parent_device_id = mock_parent.id, - parent_assigned_child_key = HEM8_DEVICES[4].child_key -}) - -local function test_init() - test.mock_device.add_test_device(mock_parent) - test.mock_device.add_test_device(mock_child_prod) - test.mock_device.add_test_device(mock_child_sald_con) - test.mock_device.add_test_device(mock_child_sald_prod) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Added lifecycle event should create children for parent device", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "added" }) - - for _, child in ipairs(HEM8_DEVICES) do - if(child["child_key"]) then - mock_parent:expect_device_create( - { - type = "EDGE_CHILD", - label = child.name, - profile = child.profile, - parent_device_id = mock_parent.id, - parent_assigned_child_key = child.child_key - } - ) - end - end - -- Refresh - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - end -) - -test.register_coroutine_test( - "Configure should configure all necessary attributes", - function() - test.socket.zwave:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_parent.id, "doConfigure" }) - - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 111, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 112, size = 4, configuration_value = 300}) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({parameter_number = 113, size = 4, configuration_value = 300}) - )) - mock_parent:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Power meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 7 and endpoint ~= 8 and endpoint ~= 9 and endpoint ~= 10 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 27 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.powerMeter.power({ value = 27, unit = "W" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Energy meter report should be handled", - function() - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - local component = "main" - if endpoint ~= 7 and endpoint ~= 8 and endpoint ~= 9 and endpoint ~= 10 then - component = string.format("clamp%d", endpoint) - end - test.socket.zwave:__queue_receive({ - mock_parent.id, - Meter:Report({ - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, { - encap = zw.ENCAP.AUTO, - src_channel = endpoint, - dst_channels = {0} - }) - }) - if(device["child_key"]) then - if(device["child_key"] == "pro") then - test.socket.capability:__expect_send( - mock_child_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-pro") then - test.socket.capability:__expect_send( - mock_child_sald_prod:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - elseif (device["child_key"] == "sald-con") then - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - else - test.socket.capability:__expect_send( - mock_parent:generate_test_message(component, capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - end - end - end - end -) - -test.register_coroutine_test( - "Report consumption and power consumption report after 15 minutes", function() - -- set time to trigger power consumption report - local current_time = os.time() - 60 * 20 - mock_child_sald_con:set_field(LAST_REPORT_TIME, current_time) - - test.socket.zwave:__queue_receive( - { - mock_child_sald_con.id, - zw_test_utils.zwave_test_build_receive_command(Meter:Report( - { - scale = Meter.scale.electric_meter.KILOWATT_HOURS, - meter_value = 5 - }, - { - encap = zw.ENCAP.AUTO, - src_channel = 9, - dst_channels = {0} - } - )) - } - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", capabilities.energyMeter.energy({ value = 5, unit = "kWh" })) - ) - - test.socket.capability:__expect_send( - mock_child_sald_con:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ deltaEnergy = 0.0, energy = 5000 })) - ) - end -) - -test.register_coroutine_test( - "Handle preference: thresholdCheck (parameter 3) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - thresholdCheck = 0 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 3, - configuration_value = 0, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdTotal (parameter 4) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 4, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseA (parameter 5) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 5, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseB (parameter 6) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseB = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 6, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWThresholdPhaseC (parameter 7) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWThresholdPhaseC = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 7, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdTotal (parameter 8) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdTotal = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 8, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseA (parameter 9) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseA = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 9, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseB (parameter 10) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseB = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 10, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWThresholdPhaseC (parameter 11) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWThresholdPhaseC = 3500 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 11, - configuration_value = 3500, - size = 2 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imtWPctThresholdTotal (parameter 12) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imtWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 12, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseA (parameter 13) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 13, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseB (parameter 14) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseB = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 14, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: imWPctThresholdPhaseC (parameter 15) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - imWPctThresholdPhaseC = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 15, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdTotal (parameter 16) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdTotal = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 16, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdPhaseA (parameter 17) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseA = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 17, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: exWPctThresholdPhaseB (parameter 18) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseB = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 18, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: thresholdCheck (exWPctThresholdPhaseC 19) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - exWPctThresholdPhaseC = 50 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 19, - configuration_value = 50, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Handle preference: autoRootDeviceReport (parameter 32) in infoChanged", - function() - test.socket.device_lifecycle:__queue_receive( - mock_parent:generate_info_changed({ - preferences = { - autoRootDeviceReport = 1 - } - }) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Configuration:Set({ - parameter_number = 32, - configuration_value = 1, - size = 1 - }) - ) - ) - end -) - -test.register_coroutine_test( - "Refresh sends commands to all components including base device", - function() - -- refresh commands for zwave devices do not have guaranteed ordering - test.socket.zwave:__set_channel_ordering("relaxed") - - for _, device in ipairs(HEM8_DEVICES) do - for _, endpoint in ipairs(device.endpoints) do - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.WATTS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_parent, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}, { - encap = zw.ENCAP.AUTO, - src_channel = 0, - dst_channels = { endpoint } - }) - ) - ) - end - end - - test.socket.capability:__queue_receive({ - mock_parent.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - }) - end -) - -test.run_registered_tests() \ No newline at end of file From 20f23125a6d36247f04939c2997d5f6dd08f3fbf Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 18 Jul 2025 13:26:14 -0700 Subject: [PATCH 040/449] WWSTCERT-7067 Zemismart WiFi Smart Switch WWSTCERT-7070 Zemismart WiFi Smart Switch WWSTCERT-7073 Zemismart WiFi Smart Switch --- .../SmartThings/matter-switch/fingerprints.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index afa4c014b2..22156c0bd2 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2667,6 +2667,21 @@ matterManufacturer: vendorId: 0x139C productId: 0x0388 deviceProfileName: matter-bridge + - id: "5020/43778" + deviceLabel: Zemismart WiFi Smart Switch + vendorId: 0x139C + productId: 0xAB02 + deviceProfileName: switch-binary + - id: "5020/43779" + deviceLabel: Zemismart WiFi Smart Switch + vendorId: 0x139C + productId: 0xAB03 + deviceProfileName: switch-binary + - id: "5020/43781" + deviceLabel: Zemismart WiFi Smart Switch + vendorId: 0x139C + productId: 0xAB05 + deviceProfileName: switch-binary #TUO - id: "5150/1" deviceLabel: "TUO Smart Button" From d93fb9a63f3256545e18f976989170909468a3cd Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 18 Jul 2025 13:29:43 -0700 Subject: [PATCH 041/449] WWSTCERT-7020 Eve Thermo --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index ddeba331e0..3f5cc5f8c9 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -22,6 +22,11 @@ matterManufacturer: vendorId: 0x130A productId: 0x004F deviceProfileName: thermostat-heating-only-nostate + - id: "4874/125" + deviceLabel: Eve Thermo + vendorId: 0x130A + productId: 0x007D + deviceProfileName: thermostat-heating-only-nostate #Habi - id: "5415/1" deviceLabel: habi Matter Wireless Room Thermostat From 5c025576f59b1862a505085064384bd9706d6eda Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 18 Jul 2025 13:32:47 -0700 Subject: [PATCH 042/449] WWSTCERT-7060 Lock Matter --- drivers/SmartThings/matter-lock/fingerprints.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 93c6beed47..7ad02e6ac7 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -61,7 +61,12 @@ matterManufacturer: deviceLabel: Level Lock Pro vendorId: 0x129F productId: 0x0004 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock-nocodes-notamper + - id: "4767/5" + deviceLabel: Level Lock (Matter) + vendorId: 0x129F + productId: 0x0005 + deviceProfileName: lock-nocodes-notamper #Nuki - id: "4957/161" deviceLabel: Nuki Smart Lock Ultra From 2728cdcd1ef7914f76731382d0870ab08701d998 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 21 Jul 2025 14:04:33 -0700 Subject: [PATCH 043/449] WWSTCERT-7091 ULTRALOQ Bolt Z-Wave Smart Deadbolt --- drivers/SmartThings/zwave-lock/fingerprints.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/zwave-lock/fingerprints.yml b/drivers/SmartThings/zwave-lock/fingerprints.yml index 29d07e956c..a472db1613 100644 --- a/drivers/SmartThings/zwave-lock/fingerprints.yml +++ b/drivers/SmartThings/zwave-lock/fingerprints.yml @@ -375,6 +375,13 @@ zwaveManufacturer: productType: 0x0005 productId: 0x0001 deviceProfileName: lock-battery + # ULTRALOQ + - id: "1106/4/2" + deviceLabel: ULTRALOQ Bolt Z-Wave Smart Deadbolt + manufacturerId: 0x0452 + productId: 0x0002 + productType: 0x0004 + deviceProfileName: base-lock zwaveGeneric: - id: "GenericZwaveLock" deviceLabel: Door Lock From 61925a5793829dfc40d4f9ddbb78ae7d3b029f25 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 21 Jul 2025 14:31:21 -0700 Subject: [PATCH 044/449] WWSTCERT-6967 Shelly 2PM Gen3 --- 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 22156c0bd2..a4af10d789 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -765,6 +765,11 @@ matterManufacturer: vendorId: 0x1490 productId: 0x1029 deviceProfileName: plug-power-energy-powerConsumption + - id: 5264/4101 + deviceLabel: Shelly 2PM Gen3 + vendorId: 0x1490 + productId: 0x1005 + deviceProfileName: plug-power-energy-powerConsumption #SONOFF - id: "SONOFF MINIR4M" deviceLabel: Smart Plug-in Unit From daa99afa2b86964ba2c6ee55a09c96d5807da578 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 5 Jun 2025 11:24:55 -0700 Subject: [PATCH 045/449] WWSTCERT-6433 LG Smart Plug --- 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 a4af10d789..17535887d1 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -555,6 +555,11 @@ matterManufacturer: vendorId: 0x102E productId: 0x2251 deviceProfileName: 2-button-battery + - id: "4142/8800" + deviceLabel: LG Smart Plug + vendorId: 0x102E + productId: 0x2260 + deviceProfileName: plug-power #Lifx - id: "5155/161" deviceLabel: LIFX Neon Outdoor From c86d52edc618f611df8abc5aee7391d6ad562563 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 18 Jun 2025 08:05:40 -0700 Subject: [PATCH 046/449] WWSTCERT-6423 LG Smart Wall Switch (3 Button) WWSTCERT-6426 LG Smart Wall Switch (2 Button) WWSTCERT-6429 LG Smart Wall Switch (1 Button) --- .../SmartThings/matter-switch/fingerprints.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 17535887d1..36487d6907 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -560,6 +560,21 @@ matterManufacturer: vendorId: 0x102E productId: 0x2260 deviceProfileName: plug-power + - id: "4142/8704" + deviceLabel: LG Smart Wall Switch (1 Button) + vendorId: 0x102E + productId: 0x2200 + deviceProfileName: light-binary + - id: "4142/8705" + deviceLabel: LG Smart Wall Switch (2 Button) + vendorId: 0x102E + productId: 0x2201 + deviceProfileName: light-binary + - id: "4142/8706" + deviceLabel: LG Smart Wall Switch (3 Button) + vendorId: 0x102E + productId: 0x2202 + deviceProfileName: light-binary #Lifx - id: "5155/161" deviceLabel: LIFX Neon Outdoor From 022f811ea0d29b4b78a1ef71001aaf108d784c53 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 25 Jul 2025 13:23:35 -0700 Subject: [PATCH 047/449] WWSTCERT-7140 Switching Actuator S1-R (Series 2) --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 9a175c5af8..5571663718 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -595,6 +595,11 @@ zigbeeManufacturer: manufacturer: ubisys model: "S2 (5502)" deviceProfileName: switch-power-firmware-2 + - id: "ubisys/1151" + deviceLabel: Switching Actuator S1-R (Series 2) + manufacturer: ubisys + model: "1151" + deviceProfileName: switch-power - id: "Megaman/AD-DimmableLight3001" deviceLabel: INGENIUM Light manufacturer: Megaman From 7b87f52dc534db5b1553bceefb29067d28d5ada4 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 25 Jul 2025 13:19:26 -0700 Subject: [PATCH 048/449] WWSTCERT-7132 SmartWave Motorized Roller Shades 100% Blackout Flex --- .../matter-window-covering/fingerprints.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 467db1ff7d..b761cc38ea 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -24,7 +24,13 @@ matterManufacturer: deviceLabel: Eve MotionBlinds for Curtains vendorId: 0x130A productId: 0x0064 - deviceProfileName: window-covering-battery + deviceProfileName: window-covering-battery +# SmartWave + - id: "5376/10001" + deviceLabel: SmartWave Motorized Roller Shades 100 Blackout Flex + vendorId: 0x1500 + productId: 0x2711 + deviceProfileName: window-covering-battery #Zemismart - id: "Zemismart MT01 Slide Curtain" deviceLabel: Zemismart MT01 Slide Curtain @@ -65,7 +71,7 @@ matterManufacturer: deviceLabel: Zemismart ZM24A Smart Curtain vendorId: 0x139C productId: 0xFA17 - deviceProfileName: window-covering + deviceProfileName: window-covering #WISTAR - id: "5207/17" deviceLabel: WISTAR WSER40 Smart Tubular Motor From c81adf383d9f37971f352d7944e9dbc138db1dfe Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Mon, 28 Jul 2025 15:12:05 -0500 Subject: [PATCH 049/449] Add missing quotes in fingerprint ID for Shelly 2PM Gen3 --- drivers/SmartThings/matter-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 36487d6907..784c38a75d 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -785,7 +785,7 @@ matterManufacturer: vendorId: 0x1490 productId: 0x1029 deviceProfileName: plug-power-energy-powerConsumption - - id: 5264/4101 + - id: "5264/4101" deviceLabel: Shelly 2PM Gen3 vendorId: 0x1490 productId: 0x1005 From 287f556aac42d2508e2f1936255415a6d4d2dbe5 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 25 Jul 2025 14:03:38 -0700 Subject: [PATCH 050/449] WWSTCERT-7192 Smart Presence Sensor --- drivers/SmartThings/matter-sensor/fingerprints.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index a5ab7a5e2e..1da15b38af 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -15,7 +15,7 @@ matterManufacturer: deviceLabel: "Door/window contact II [M]" vendorId: 0x1209 productId: 0x3015 - deviceProfileName: contact-button-battery + deviceProfileName: contact-button-battery #Elko - id: "5170/4098" deviceLabel: RFWD-100/MT @@ -68,7 +68,7 @@ matterManufacturer: deviceLabel: Smart co sensor vendorId: 0x120B productId: 0x1007 - deviceProfileName: co + deviceProfileName: co # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor @@ -81,6 +81,12 @@ matterManufacturer: vendorId: 0x102E productId: 0x2010 deviceProfileName: aqs-temp-humidity-co2-pm1-pm25-pm10-meas + # Meross + - id: "4933/16897" + deviceLabel: Smart Presence Sensor + vendorId: 0x1345 + productId: 0x4201 + deviceProfileName: motion-illuminance # Neo - id: "4991/1122" deviceLabel: Door Sensor @@ -130,7 +136,7 @@ matterManufacturer: deviceLabel: LG Motion Sensor vendorId: 0x102E productId: 0x2030 - deviceProfileName: motion-illuminance-battery + deviceProfileName: motion-illuminance-battery # Siterwell - id: "4736/847" deviceLabel: Siterwell Door Window Sensor @@ -141,7 +147,7 @@ matterManufacturer: deviceLabel: Siterwell Smart Smoke Alarm vendorId: 0x1280 productId: 0x8014 - deviceProfileName: smoke + deviceProfileName: smoke - id: "4736/32789" deviceLabel: Siterwell Smart Carbon Monoxide Alarm vendorId: 0x1280 From 6cb67e13cecfdbc8aadf2e86117f98e90bb85cff Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 29 Jul 2025 11:26:16 -0700 Subject: [PATCH 051/449] WWSTCERT-7315 SmartSetup Smart Multi-Switch WWSTCERT-7318 SmartSetup Smart Switch --- drivers/SmartThings/matter-switch/fingerprints.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index d42a735b29..0f7ee95479 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -790,6 +790,19 @@ matterManufacturer: vendorId: 0x1490 productId: 0x1005 deviceProfileName: plug-power-energy-powerConsumption + +#SmartSetup + - id: "5385/4" + deviceLabel: SmartSetup Smart Multi-Switch + vendorId: 0x1509 + productId: 0x0004 + deviceProfileName: switch-binary + - id: "5385/1" + deviceLabel: SmartSetup Smart Switch + vendorId: 0x1509 + productId: 0x0001 + deviceProfileName: switch-binary + #SONOFF - id: "SONOFF MINIR4M" deviceLabel: Smart Plug-in Unit From c763142939fa3470b8ac4ba0711a8657572df8b8 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 29 Jul 2025 11:30:09 -0700 Subject: [PATCH 052/449] WWSTCERT-7310 eufy Permanent Outdoor Lights S4 --- drivers/SmartThings/matter-switch/fingerprints.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 0f7ee95479..850ce86d1d 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -177,6 +177,13 @@ matterManufacturer: productId: 0x1003 deviceProfileName: 4-button-battery +#Eufy + - id: "5427/19" + deviceLabel: eufy Permanent Outdoor Lights S4 + vendorId: 0x1533 + productId: 0x0013 + deviceProfileName: light-color-level-2200K-6500K + #Eve - id: "Eve/Energy/US" deviceLabel: Eve Energy From 8c4265f129b2666bbd540c065955b8e242e29aff Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 29 Jul 2025 11:37:48 -0700 Subject: [PATCH 053/449] WWSTCERT-7339 Sense by MACO | Door WWSTCERT-7342 Sense by MACO | Casement WWSTCERT-7336 Sense by MACO | Window Pro T&T --- .../SmartThings/matter-sensor/fingerprints.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 1da15b38af..c5da938889 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -81,6 +81,22 @@ matterManufacturer: vendorId: 0x102E productId: 0x2010 deviceProfileName: aqs-temp-humidity-co2-pm1-pm25-pm10-meas + # Maco + - id: "5447/1001" + deviceLabel: Sense by MACO | Window Pro T&T + vendorId: 0x1547 + productId: 0x03E9 + deviceProfileName: contact-battery + - id: "5447/1003" + deviceLabel: Sense by MACO | Casement + vendorId: 0x1547 + productId: 0x03EB + deviceProfileName: contact-battery + - id: "5447/1004" + deviceLabel: Sense by MACO | Door + vendorId: 0x1547 + productId: 0x03EC + deviceProfileName: contact-battery # Meross - id: "4933/16897" deviceLabel: Smart Presence Sensor From 33c9f3c55edff7650102c059b5ceab4901b4a14a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 29 Jul 2025 11:33:44 -0700 Subject: [PATCH 054/449] WWSTCERT-7328 Mamaba Wi-Fi Curtain --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index fc9f55bfa4..e831100e93 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -25,6 +25,12 @@ matterManufacturer: vendorId: 0x130A productId: 0x0064 deviceProfileName: window-covering-battery +# Mamaba + - id: "4965/4097" + deviceLabel: Wi-Fi Curtain + vendorId: 0x1365 + productId: 0x1001 + deviceProfileName: window-covering # SmartWave - id: "5376/10001" deviceLabel: SmartWave Motorized Roller Shades 100 Blackout Flex From 4859e5aaf7ba2667899f5be85f3a61b53e8c5cb3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 31 Jul 2025 10:59:08 -0700 Subject: [PATCH 055/449] WWSTCERT-7346 ubisys Dimmer D1 (#2309) * WWSTCERT-7346 ubisys Dimmer D1 * change profile * change join name --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 5571663718..e449e1f194 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -586,10 +586,10 @@ zigbeeManufacturer: model: "TRADFRI bulb E27 WW clear 250lm" deviceProfileName: on-off-level - id: "ubisys/D1 (5503)" - deviceLabel: INGENIUM Light + deviceLabel: Dimmer D1 manufacturer: ubisys model: "D1 (5503)" - deviceProfileName: on-off-level + deviceProfileName: switch-level-power - id: "ubisys/S2 (5502)" deviceLabel: ubisys S2 manufacturer: ubisys From df0d62b9308a8b88553717612199d92697c6f96e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 29 Jul 2025 11:44:12 -0700 Subject: [PATCH 056/449] WWSTCERT-7196 Eve Shutter Switch --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index e831100e93..92b7fc47f0 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -25,6 +25,11 @@ matterManufacturer: vendorId: 0x130A productId: 0x0064 deviceProfileName: window-covering-battery + - id: "4874/96" + deviceLabel: Eve Shutter Switch + vendorId: 0x130A + productId: 0x0060 + deviceProfileName: window-covering # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain From 5740c815f80d33bd54c6d7935dd604c635b351a3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 30 Jul 2025 13:20:13 -0700 Subject: [PATCH 057/449] WWSTCERT-7351 Govee Smart Bulb --- drivers/SmartThings/matter-switch/fingerprints.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 850ce86d1d..cd03be7663 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -411,6 +411,14 @@ matterManufacturer: vendorId: 0x1339 productId: 0x0016 deviceProfileName: light-color-level-2000K-7000K + +# Govee + - id: "4999/24584" + deviceLabel: Govee Smart Bulb + vendorId: 0x1387 + productId: 0x6008 + deviceProfileName: light-color-level-2700K-6500K + #Ledvance - id: "4489/843" deviceLabel: Matter Filament RGBW From e4c114310efe5353a0e9973d02aa8a9983d1359b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 30 Jul 2025 13:22:28 -0700 Subject: [PATCH 058/449] WWSTCERT-7376 Edishine Smart Led Bulb --- drivers/SmartThings/matter-switch/fingerprints.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index cd03be7663..d6134e798f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -154,6 +154,13 @@ matterManufacturer: productId: 0x1388 deviceProfileName: 12-button-keyboard +#Edishine + - id: "5458/3328" + deviceLabel: Edishine Smart Led Bulb + vendorId: 0x1552 + productId: 0x0D00 + deviceProfileName: light-color-level + #ELEGRP - id: "5158/3" deviceLabel: ELEGRP Smart Mini Plug From feb3065bf245fb0da265b98a2b4d88cb13078845 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 5 Aug 2025 10:52:06 -0500 Subject: [PATCH 059/449] Revert "add greater energy profiling logic for switches (#2199)" This reverts commit f3642cf13230d9ece56740306b1d74b7b7c6e42c. --- ...ht-level-power-energy-powerConsumption.yml | 44 +-- .../light-power-energy-powerConsumption.yml | 32 +- .../SmartThings/matter-switch/src/init.lua | 335 ++++++------------ .../test/test_aqara_climate_sensor_w100.lua | 1 - .../src/test/test_aqara_light_switch_h2.lua | 23 +- .../src/test/test_electrical_sensor.lua | 71 +--- .../src/test/test_matter_button.lua | 1 - .../src/test/test_matter_light_fan.lua | 1 - .../src/test/test_matter_multi_button.lua | 1 - .../test_matter_multi_button_switch_mcd.lua | 2 - .../test/test_matter_switch_device_types.lua | 18 - .../test_multi_switch_parent_child_lights.lua | 4 - .../test_multi_switch_parent_child_plugs.lua | 40 --- 13 files changed, 173 insertions(+), 400 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml index e81d7dc700..f6c45ed1f7 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml @@ -1,24 +1,24 @@ name: light-level-power-energy-powerConsumption components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - config: - values: - - key: "level.value" - range: [1, 100] - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml index 791b7a0692..1e64f63594 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml @@ -1,18 +1,18 @@ name: light-power-energy-powerConsumption components: -- id: main - capabilities: - - id: switch - version: 1 - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light + - id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b2836c4cc4..c4e9d6dceb 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -50,6 +50,7 @@ local CURRENT_HUESAT_ATTR_MAX = 254 -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" local COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" local COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" @@ -62,8 +63,7 @@ local COLOR_MODE = "__color_mode" local updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil }, - { current_field_name = "__energy_management_endpoint", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil } } local HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION @@ -174,21 +174,20 @@ local device_type_attribute_map = { } } -local device_overrides_per_vendor = { +local child_device_profile_overrides_per_vendor_id = { [0x1321] = { { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { - { product_id = 0x1003, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, combo_switch_button = false }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, combo_switch_button = false }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) } - } local detect_matter_thing @@ -204,12 +203,6 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt -local ELECTRICAL_SENSOR_EPS = "__ELECTRICAL_SENSOR_EPS" - -local profiling_data = { - ELECTRICAL_TOPOLOGY = "__ELECTRICAL_TOPOLOGY", -} - -- Return an ISO-8061 timestamp in UTC local function iso8061Timestamp(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) @@ -237,30 +230,28 @@ local function send_import_poll_report(device, latest_total_imported_energy_wh) energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end - -- Report the energy consumed during the time interval on the first device that supports it. The unit of these values should be 'Wh' - if device:get_parent_device() and device:get_parent_device():supports_capability(capabilities.powerConsumptionReport) then - device = device:get_parent_device() - elseif device:get_parent_device() ~= nil then - device = device:get_parent_device():get_child_list()[1] + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + if not device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + else + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end - local component = device.profile.components["main"] - device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - end local function create_poll_report_schedule(device) local import_timer = device.thread:call_on_schedule( device:get_field(IMPORT_REPORT_TIMEOUT), function() - local total_imported_energy = 0 - for _, energy_report in pairs(device:get_field(TOTAL_IMPORTED_ENERGY)) do - total_imported_energy = total_imported_energy + energy_report - end - send_import_poll_report(device, total_imported_energy) + send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)) end, "polling_import_report_schedule_timer" ) device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) @@ -284,8 +275,10 @@ local function set_poll_report_timer_and_schedule(device, is_cumulative_report) local second_timestamp = os.time() local report_interval_secs = second_timestamp - first_timestamp device:set_field(IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumptionReport - if device:supports_capability(capabilities.powerConsumptionReport) then + -- the poll schedule is only needed for devices that support powerConsumption + -- and enable powerConsumption when energy management is defined in root endpoint(0). + if device:supports_capability(capabilities.powerConsumptionReport) or + device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then create_poll_report_schedule(device) end device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) @@ -328,19 +321,6 @@ local function create_multi_press_values_list(size, supportsHeld) return list end --- get a list of endpoints for a specified device type. -local function get_endpoints_by_dt(device, device_type_id) - local dt_eps = {} - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type_id then - table.insert(dt_eps, ep.endpoint_id) - end - end - end - return dt_eps -end - local function tbl_contains(array, value) for _, element in ipairs(array) do if element == value then @@ -405,13 +385,21 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. local function device_type_supports_button_switch_combination(device, endpoint_id) - for _, fingerprint in ipairs(device_overrides_per_vendor[AQARA_MANUFACTURER_ID] or {}) do - if fingerprint.product_id == device.manufacturer_info.product_id and fingerprint.combo_switch_button == false then - return false -- For Aqara Dimmer Switch with Button. + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then + for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[0x115F]) do + if device.manufacturer_info.product_id == fingerprint.product_id then + return false -- For Aqara Dimmer Switch with Button. + end + end + return true + end + end end end - local dimmable_eps = get_endpoints_by_dt(device, DIMMABLE_LIGHT_DEVICE_TYPE_ID) - return tbl_contains(dimmable_eps, endpoint_id) + return false end local function get_first_non_zero_endpoint(endpoints) @@ -492,42 +480,43 @@ local function check_field_name_updates(device) end end -local function assign_switch_profile(device, switch_ep, is_child_device, electrical_tags) +local function assign_child_profile(device, child_ep) local profile + for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == switch_ep then + if ep.endpoint_id == child_ep then -- Some devices report multiple device types which are a subset of -- a superset device type (For example, Dimmable Light is a superset of -- On/Off light). This mostly applies to the four light types, so we will want -- to match the profile for the superset device type. This can be done by -- matching to the device type with the highest ID - -- Note: Electrical Sensor does not follow the above logic, so it's ignored local id = 0 for _, dt in ipairs(ep.device_types) do - if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then - id = math.max(id, dt.device_type_id) - end + id = math.max(id, dt.device_type_id) end profile = device_type_profile_map[id] break end end - if electrical_tags ~= nil and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then - profile = string.gsub(profile, "-binary", "") .. electrical_tags - end - - if is_child_device then - -- Check if child device has an overridden child profile that differs from the child's generic device type profile - for _, fingerprint in ipairs(device_overrides_per_vendor[device.manufacturer_info.vendor_id] or {}) do - if device.manufacturer_info.product_id == fingerprint.product_id and profile == fingerprint.initial_profile then - return fingerprint.target_profile - end + -- Check if device has an overridden child profile that differs from the profile that would match + -- the child's device type for the following two cases: + -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) + -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is + -- determined in the "for" loop above (e.g., light-binary) + -- 2. The selected profile for the child device matches the initial profile defined in + -- child_device_profile_overrides + for id, vendor in pairs(child_device_profile_overrides_per_vendor_id) do + for _, fingerprint in ipairs(vendor) do + if device.manufacturer_info.product_id == fingerprint.product_id and + ((device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then + return fingerprint.target_profile end - -- default to "switch-binary" if no child profile is found - return profile or "switch-binary" + end end - return profile + + -- default to "switch-binary" if no profile is found + return profile or "switch-binary" end local function configure_buttons(device) @@ -609,8 +598,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint local name = string.format("%s %d", device.label, num_switch_server_eps) - local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[ep] - local child_profile = assign_switch_profile(device, ep, true, electrical_tags) + local child_profile = assign_child_profile(device, ep) driver:try_create_device( { type = "EDGE_CHILD", @@ -622,6 +610,10 @@ local function build_child_switch_profiles(driver, device, main_endpoint) } ) parent_child_device = true + if _ == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + end end end end @@ -656,7 +648,8 @@ local function handle_light_switch_with_onOff_server_clusters(device, main_endpo end end -local function initialize_buttons(driver, device, main_endpoint) +local function initialize_buttons_and_switches(driver, device, main_endpoint) + local profile_found = false local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then build_button_profile(device, main_endpoint, #button_eps) @@ -664,59 +657,32 @@ local function initialize_buttons(driver, device, main_endpoint) -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field build_button_component_map(device, main_endpoint, button_eps) configure_buttons(device) - return true + profile_found = true end -end -local function collect_and_store_electrical_sensor_info(driver, device) - local el_dt_eps = get_endpoints_by_dt(device, ELECTRICAL_SENSOR_ID) - local electrical_sensor_eps = {} - local avail_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - for _, ep in ipairs(device.endpoints) do - if tbl_contains(el_dt_eps, ep.endpoint_id) then - local electrical_ep_info = {} - electrical_ep_info.endpoint_id = ep.endpoint_id - for _, cluster in ipairs(ep.clusters) do - if cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then - electrical_ep_info.energy = true - elseif cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then - electrical_ep_info.power = true - elseif cluster.cluster_id == clusters.PowerTopology.ID then - electrical_ep_info.topology = cluster.feature_map - if cluster.feature_map == clusters.PowerTopology.types.Feature.SET_TOPOLOGY then - avail_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) - electrical_ep_info.availableEndpoints = false - end - end - end - table.insert(electrical_sensor_eps, electrical_ep_info) - end - end - if #avail_eps_req.info_blocks ~= 0 then - device:send(avail_eps_req) - end - device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -end - -local function initialize_switches(driver, device, main_endpoint) -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled -- while building switch child profiles local num_switch_server_eps = build_child_switch_profiles(driver, device, main_endpoint) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. -- Note: since their device type isn't supported, these devices join as a matter-thing. if num_switch_server_eps > 0 and detect_matter_thing(device) then handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - return true + profile_found = true end + return profile_found end local function detect_bridge(device) - return #get_endpoints_by_dt(device, AGGREGATOR_DEVICE_TYPE_ID) > 0 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false end local function device_init(driver, device) @@ -733,11 +699,6 @@ local function device_init(driver, device) if ep.endpoint_id ~= main_endpoint then local id = 0 for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == ELECTRICAL_SENSOR_ID then - for _, attr in pairs(device_type_attribute_map[ELECTRICAL_SENSOR_ID]) do - device:add_subscribed_attribute(attr) - end - end id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do @@ -755,31 +716,34 @@ local function device_init(driver, device) 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) - if profiling_data_still_required(device) then return end - local main_endpoint = find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local button_profile_found = initialize_buttons(driver, device, main_endpoint) - local switch_profile_found = initialize_switches(driver, device, main_endpoint) - if button_profile_found or switch_profile_found then + local profile_found = initialize_buttons_and_switches(driver, device, main_endpoint) + if device:get_field(IS_PARENT_CHILD_DEVICE) then + device:set_find_child(find_child) + end + if profile_found then return end local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local level_eps = device:get_endpoints(clusters.LevelControl.ID) + local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) local profile_name = nil - if #valve_eps > 0 then + local level_support = "" + if #level_eps > 0 then + level_support = "-level" + end + if #energy_eps > 0 and #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" + elseif #energy_eps > 0 then + profile_name = "plug" .. level_support .. "-energy-powerConsumption" + elseif #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power" + elseif #valve_eps > 0 then profile_name = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then @@ -790,24 +754,6 @@ local function match_profile(driver, device) end if profile_name then device:try_update_metadata({ profile = profile_name }) - return - end - - -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device - local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[main_endpoint] - profile_name = assign_switch_profile(device, main_endpoint, false, electrical_tags) - -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since - -- these may lose fingerprinted Kelvin ranges when dynamically profiled. - if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then - if profile_name == "light-level" and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then - profile_name = "light-level-motion" - end - device:try_update_metadata({profile = profile_name}) - end - - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) end end @@ -1179,56 +1125,26 @@ local function occupancy_attr_handler(driver, device, ib, response) device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end -local function available_endpoints_handler(driver, device, ib, response) - local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) - local avail_eps_req_complete = true - local electrical_tags_per_ep = {} - table.sort(ib.data.elements) - for _, info in ipairs(electrical_sensor_eps or {}) do - if info.endpoint_id == ib.endpoint_id then - info.availableEndpoints = ib.data.elements - elseif info.availableEndpoints == false then -- an endpoint is found that hasn't been updated through this handler. - avail_eps_req_complete = false - end - if avail_eps_req_complete then - local electrical_tags = "" - if info.power then electrical_tags = electrical_tags .. "-power" end - if info.energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - electrical_tags_per_ep[info.availableEndpoints[1].value] = electrical_tags -- set tags on first available endpoint - end - end - if avail_eps_req_complete == false then - device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) - else - local topology_data = {} - topology_data.topology = clusters.PowerTopology.types.Feature.SET_TOPOLOGY - topology_data.tags_on_ep = electrical_tags_per_ep - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, topology_data) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - match_profile(driver, device) - end -end - local function cumul_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then - local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} - total_imported_energy[ib.endpoint_id] = watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) - device:emit_component_event(component, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + end end end local function per_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then - local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} - local latest_energy_report = total_imported_energy[ib.endpoint_id] or 0 - total_imported_energy[ib.endpoint_id] = latest_energy_report + watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) - device:emit_component_event(component, capabilities.energyMeter.energy({ value = total_imported_energy[ib.endpoint_id], unit = "Wh" })) + local latest_energy_report = device:get_field(TOTAL_IMPORTED_ENERGY) or 0 + local summed_energy_report = latest_energy_report + watt_hour_value + device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) + device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) end end @@ -1279,12 +1195,14 @@ local function short_release_event_handler(driver, device, ib, response) end local function active_power_handler(driver, device, ib, response) - local component = device.profile.components["main"] if ib.data.value then local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:emit_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - device:emit_component_event(component, capabilities.powerMeter.power({ value = 0, unit = "W"})) + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end end end @@ -1398,22 +1316,8 @@ local function device_added(driver, device) if device.network_type == device_lib.NETWORK_TYPE_CHILD then local req = clusters.OnOff.attributes.OnOff:read(device) device:send(req) - elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then - collect_and_store_electrical_sensor_info(driver, device) - local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) - if #electrical_sensor_eps == 0 then - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = false, tags_on_ep = {}}) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - elseif electrical_sensor_eps[1].topology == clusters.PowerTopology.types.Feature.NODE_TOPOLOGY then - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - local electrical_tags = "" - if electrical_sensor_eps[1].power then electrical_tags = electrical_tags .. "-power" end - if electrical_sensor_eps[1].energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, tags_on_ep = {[switch_eps[1]] = electrical_tags}}) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - end end + -- call device init in case init is not called after added due to device caching device_init(driver, device) end @@ -1571,9 +1475,6 @@ local matter_driver_template = { [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), }, - [clusters.PowerTopology.ID] = { - [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = available_endpoints_handler, - }, [clusters.ValveConfigurationAndControl.ID] = { [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler 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 06c726854a..830a9b99f8 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 @@ -141,7 +141,6 @@ local function test_init() end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - aqara_mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) 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 2e5aef1f2d..369689e181 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 @@ -38,8 +38,7 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, - {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -178,12 +177,10 @@ local function test_init() end end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - - -- Test added -> doConfigure logic - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - aqara_mock_device:expect_metadata_update({ profile = "4-button" }) + test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" + test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} + test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport @@ -280,7 +277,7 @@ test.register_coroutine_test( { -- don't use "aqara_mock_children[aqara_child1_ep].id," -- because energy management is at the root endpoint. - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) } ) @@ -292,7 +289,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) } ) @@ -305,7 +302,7 @@ test.register_coroutine_test( -- This is because related variable settings are required in set_poll_report_timer_and_schedule(). test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) } ) @@ -316,8 +313,10 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_39) + aqara_mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( + aqara_mock_device, 1, cumulative_report_val_39 + ) } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 4f88338185..e812bdfdaa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" @@ -43,7 +42,6 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -53,30 +51,10 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = 2, clusters = { { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug - } - }, - { - endpoint_id = 3, - clusters = { - { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY - }, - device_types = { - { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor - } - }, - { - endpoint_id = 4, - clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - }, - device_types = { - { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug } }, }, @@ -102,20 +80,16 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug - { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor } }, }, }) local subscribed_attributes_periodic = { - clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } @@ -669,30 +643,9 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) - read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) - test.socket.matter:__expect_send({ mock_device.id, read_req }) - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - 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" }) - test.wait_for_events() - test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) - test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "nil 2", - profile = "plug-level-energy-powerConsumption", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", 4) - }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init } ) @@ -700,21 +653,9 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) - local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) - test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) - local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) - for i, cluster in ipairs(subscribed_attributes_periodic) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_periodic)) - end - end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.wait_for_events() - test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) + mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init_periodic } ) @@ -751,7 +692,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) } }, { 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 8d10daea11..f34f432ec7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -63,7 +63,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) 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 b211bf8f53..7d3211bdfb 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 @@ -90,7 +90,6 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 7a5c39d4c2..624a3ab205 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 @@ -122,7 +122,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) 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 3c0c32037a..139c519dbf 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -198,7 +198,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -237,7 +236,6 @@ local function test_init_mcd_unsupported_switch_device_type() end end test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - mock_device_mcd_unsupported_switch_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index d2c7da6085..a35552007a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -409,7 +409,6 @@ local function test_init_parent_child_switch_types() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) - mock_device_parent_child_switch_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -427,7 +426,6 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) - mock_device_onoff:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -440,7 +438,6 @@ end local function test_init_parent_client_child_server() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) - mock_device_parent_client_child_server:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -449,7 +446,6 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) - mock_device_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -457,7 +453,6 @@ end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) - mock_device_color_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -474,9 +469,7 @@ local function test_init_mounted_on_off_control() end end test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) - mock_device_mounted_on_off_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) - mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_on_off_control) end @@ -492,16 +485,13 @@ local function test_init_mounted_dimmable_load_control() end end test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) - mock_device_mounted_dimmable_load_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) - mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) - mock_device_water_valve:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -529,9 +519,7 @@ local function test_init_parent_child_different_types() end test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) - mock_device_parent_child_different_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) - mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_different_types) @@ -546,7 +534,6 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() - mock_device_parent_child_unsupported_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -575,12 +562,7 @@ local function test_init_light_level_motion() subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) end end - test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - mock_device_light_level_motion:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. - test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) - mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) - mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_light_level_motion) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 66f019cdb0..de62865597 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -180,9 +180,7 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -248,9 +246,7 @@ local function test_init_parent_child_endpoints_non_sequential() end test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - mock_device_parent_child_endpoints_non_sequential:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index a69cdfeda7..cbf8fb0afe 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -23,8 +23,6 @@ local child_profile_override = t_utils.get_profile_definition("switch-binary.yml local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 -local child3_ep = 40 -local child4_ep = 50 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -70,24 +68,6 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug } }, - { - endpoint_id = child3_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - { - endpoint_id = child4_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - } } }) @@ -158,9 +138,7 @@ local function test_init() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -183,22 +161,6 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 4", - profile = "plug-binary", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child3_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 5", - profile = "plug-binary", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child4_ep) - }) end local mock_children_child_profile_override = {} @@ -221,9 +183,7 @@ local function test_init_child_profile_override() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) - mock_device_child_profile_override:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) - mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_child_profile_override) From 53133f8a40e0accce325d271ac2f9698b370c95d Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 5 Aug 2025 10:52:25 -0500 Subject: [PATCH 060/449] Set Energy Management Endpoint field on init --- drivers/SmartThings/matter-switch/src/init.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c4e9d6dceb..c8e3f47cec 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -593,7 +593,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) local parent_child_device = false local switch_eps = device:get_endpoints(clusters.OnOff.ID) table.sort(switch_eps) - for _, ep in ipairs(switch_eps) do + for idx, ep in ipairs(switch_eps) do if device:supports_server_cluster(clusters.OnOff.ID, ep) then num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint @@ -610,7 +610,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) } ) parent_child_device = true - if _ == 1 and string.find(child_profile, "energy") then + if idx == 1 and string.find(child_profile, "energy") then -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) end @@ -695,8 +695,15 @@ local function device_init(driver, device) end local main_endpoint = find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for _, ep in ipairs(device.endpoints) do + for idx, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= main_endpoint then + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + local child_profile = assign_child_profile(device, ep) + if idx == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + end + end local id = 0 for _, dt in ipairs(ep.device_types) do id = math.max(id, dt.device_type_id) From 014bfdaebad8d492b59a08bd18b3df69ad1ee401 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 5 Aug 2025 10:52:06 -0500 Subject: [PATCH 061/449] Revert "add greater energy profiling logic for switches (#2199)" This reverts commit f3642cf13230d9ece56740306b1d74b7b7c6e42c. --- ...ht-level-power-energy-powerConsumption.yml | 44 +-- .../light-power-energy-powerConsumption.yml | 32 +- .../SmartThings/matter-switch/src/init.lua | 335 ++++++------------ .../test/test_aqara_climate_sensor_w100.lua | 1 - .../src/test/test_aqara_light_switch_h2.lua | 23 +- .../src/test/test_electrical_sensor.lua | 71 +--- .../src/test/test_matter_button.lua | 1 - .../src/test/test_matter_light_fan.lua | 1 - .../src/test/test_matter_multi_button.lua | 1 - .../test_matter_multi_button_switch_mcd.lua | 2 - .../test/test_matter_switch_device_types.lua | 18 - .../test_multi_switch_parent_child_lights.lua | 4 - .../test_multi_switch_parent_child_plugs.lua | 40 --- 13 files changed, 173 insertions(+), 400 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml index e81d7dc700..f6c45ed1f7 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml @@ -1,24 +1,24 @@ name: light-level-power-energy-powerConsumption components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - config: - values: - - key: "level.value" - range: [1, 100] - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml index 791b7a0692..1e64f63594 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-power-energy-powerConsumption.yml @@ -1,18 +1,18 @@ name: light-power-energy-powerConsumption components: -- id: main - capabilities: - - id: switch - version: 1 - - id: powerMeter - version: 1 - - id: energyMeter - version: 1 - - id: powerConsumptionReport - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Light + - id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index b2836c4cc4..c4e9d6dceb 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -50,6 +50,7 @@ local CURRENT_HUESAT_ATTR_MAX = 254 -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +local ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" local COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" local COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" @@ -62,8 +63,7 @@ local COLOR_MODE = "__color_mode" local updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil }, - { current_field_name = "__energy_management_endpoint", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil } } local HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION @@ -174,21 +174,20 @@ local device_type_attribute_map = { } } -local device_overrides_per_vendor = { +local child_device_profile_overrides_per_vendor_id = { [0x1321] = { { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { - { product_id = 0x1003, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, combo_switch_button = false }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, combo_switch_button = false }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, combo_switch_button = false }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, combo_switch_button = false }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) } - } local detect_matter_thing @@ -204,12 +203,6 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt -local ELECTRICAL_SENSOR_EPS = "__ELECTRICAL_SENSOR_EPS" - -local profiling_data = { - ELECTRICAL_TOPOLOGY = "__ELECTRICAL_TOPOLOGY", -} - -- Return an ISO-8061 timestamp in UTC local function iso8061Timestamp(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) @@ -237,30 +230,28 @@ local function send_import_poll_report(device, latest_total_imported_energy_wh) energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end - -- Report the energy consumed during the time interval on the first device that supports it. The unit of these values should be 'Wh' - if device:get_parent_device() and device:get_parent_device():supports_capability(capabilities.powerConsumptionReport) then - device = device:get_parent_device() - elseif device:get_parent_device() ~= nil then - device = device:get_parent_device():get_child_list()[1] + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + if not device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + else + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end - local component = device.profile.components["main"] - device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - end local function create_poll_report_schedule(device) local import_timer = device.thread:call_on_schedule( device:get_field(IMPORT_REPORT_TIMEOUT), function() - local total_imported_energy = 0 - for _, energy_report in pairs(device:get_field(TOTAL_IMPORTED_ENERGY)) do - total_imported_energy = total_imported_energy + energy_report - end - send_import_poll_report(device, total_imported_energy) + send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)) end, "polling_import_report_schedule_timer" ) device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) @@ -284,8 +275,10 @@ local function set_poll_report_timer_and_schedule(device, is_cumulative_report) local second_timestamp = os.time() local report_interval_secs = second_timestamp - first_timestamp device:set_field(IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumptionReport - if device:supports_capability(capabilities.powerConsumptionReport) then + -- the poll schedule is only needed for devices that support powerConsumption + -- and enable powerConsumption when energy management is defined in root endpoint(0). + if device:supports_capability(capabilities.powerConsumptionReport) or + device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then create_poll_report_schedule(device) end device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) @@ -328,19 +321,6 @@ local function create_multi_press_values_list(size, supportsHeld) return list end --- get a list of endpoints for a specified device type. -local function get_endpoints_by_dt(device, device_type_id) - local dt_eps = {} - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type_id then - table.insert(dt_eps, ep.endpoint_id) - end - end - end - return dt_eps -end - local function tbl_contains(array, value) for _, element in ipairs(array) do if element == value then @@ -405,13 +385,21 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. local function device_type_supports_button_switch_combination(device, endpoint_id) - for _, fingerprint in ipairs(device_overrides_per_vendor[AQARA_MANUFACTURER_ID] or {}) do - if fingerprint.product_id == device.manufacturer_info.product_id and fingerprint.combo_switch_button == false then - return false -- For Aqara Dimmer Switch with Button. + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then + for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[0x115F]) do + if device.manufacturer_info.product_id == fingerprint.product_id then + return false -- For Aqara Dimmer Switch with Button. + end + end + return true + end + end end end - local dimmable_eps = get_endpoints_by_dt(device, DIMMABLE_LIGHT_DEVICE_TYPE_ID) - return tbl_contains(dimmable_eps, endpoint_id) + return false end local function get_first_non_zero_endpoint(endpoints) @@ -492,42 +480,43 @@ local function check_field_name_updates(device) end end -local function assign_switch_profile(device, switch_ep, is_child_device, electrical_tags) +local function assign_child_profile(device, child_ep) local profile + for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == switch_ep then + if ep.endpoint_id == child_ep then -- Some devices report multiple device types which are a subset of -- a superset device type (For example, Dimmable Light is a superset of -- On/Off light). This mostly applies to the four light types, so we will want -- to match the profile for the superset device type. This can be done by -- matching to the device type with the highest ID - -- Note: Electrical Sensor does not follow the above logic, so it's ignored local id = 0 for _, dt in ipairs(ep.device_types) do - if dt.device_type_id ~= ELECTRICAL_SENSOR_ID then - id = math.max(id, dt.device_type_id) - end + id = math.max(id, dt.device_type_id) end profile = device_type_profile_map[id] break end end - if electrical_tags ~= nil and (profile == "plug-binary" or profile == "plug-level" or profile == "light-binary") then - profile = string.gsub(profile, "-binary", "") .. electrical_tags - end - - if is_child_device then - -- Check if child device has an overridden child profile that differs from the child's generic device type profile - for _, fingerprint in ipairs(device_overrides_per_vendor[device.manufacturer_info.vendor_id] or {}) do - if device.manufacturer_info.product_id == fingerprint.product_id and profile == fingerprint.initial_profile then - return fingerprint.target_profile - end + -- Check if device has an overridden child profile that differs from the profile that would match + -- the child's device type for the following two cases: + -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) + -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is + -- determined in the "for" loop above (e.g., light-binary) + -- 2. The selected profile for the child device matches the initial profile defined in + -- child_device_profile_overrides + for id, vendor in pairs(child_device_profile_overrides_per_vendor_id) do + for _, fingerprint in ipairs(vendor) do + if device.manufacturer_info.product_id == fingerprint.product_id and + ((device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then + return fingerprint.target_profile end - -- default to "switch-binary" if no child profile is found - return profile or "switch-binary" + end end - return profile + + -- default to "switch-binary" if no profile is found + return profile or "switch-binary" end local function configure_buttons(device) @@ -609,8 +598,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint local name = string.format("%s %d", device.label, num_switch_server_eps) - local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[ep] - local child_profile = assign_switch_profile(device, ep, true, electrical_tags) + local child_profile = assign_child_profile(device, ep) driver:try_create_device( { type = "EDGE_CHILD", @@ -622,6 +610,10 @@ local function build_child_switch_profiles(driver, device, main_endpoint) } ) parent_child_device = true + if _ == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + end end end end @@ -656,7 +648,8 @@ local function handle_light_switch_with_onOff_server_clusters(device, main_endpo end end -local function initialize_buttons(driver, device, main_endpoint) +local function initialize_buttons_and_switches(driver, device, main_endpoint) + local profile_found = false local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then build_button_profile(device, main_endpoint, #button_eps) @@ -664,59 +657,32 @@ local function initialize_buttons(driver, device, main_endpoint) -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field build_button_component_map(device, main_endpoint, button_eps) configure_buttons(device) - return true + profile_found = true end -end -local function collect_and_store_electrical_sensor_info(driver, device) - local el_dt_eps = get_endpoints_by_dt(device, ELECTRICAL_SENSOR_ID) - local electrical_sensor_eps = {} - local avail_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - for _, ep in ipairs(device.endpoints) do - if tbl_contains(el_dt_eps, ep.endpoint_id) then - local electrical_ep_info = {} - electrical_ep_info.endpoint_id = ep.endpoint_id - for _, cluster in ipairs(ep.clusters) do - if cluster.cluster_id == clusters.ElectricalEnergyMeasurement.ID then - electrical_ep_info.energy = true - elseif cluster.cluster_id == clusters.ElectricalPowerMeasurement.ID then - electrical_ep_info.power = true - elseif cluster.cluster_id == clusters.PowerTopology.ID then - electrical_ep_info.topology = cluster.feature_map - if cluster.feature_map == clusters.PowerTopology.types.Feature.SET_TOPOLOGY then - avail_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) - electrical_ep_info.availableEndpoints = false - end - end - end - table.insert(electrical_sensor_eps, electrical_ep_info) - end - end - if #avail_eps_req.info_blocks ~= 0 then - device:send(avail_eps_req) - end - device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -end - -local function initialize_switches(driver, device, main_endpoint) -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled -- while building switch child profiles local num_switch_server_eps = build_child_switch_profiles(driver, device, main_endpoint) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. -- Note: since their device type isn't supported, these devices join as a matter-thing. if num_switch_server_eps > 0 and detect_matter_thing(device) then handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - return true + profile_found = true end + return profile_found end local function detect_bridge(device) - return #get_endpoints_by_dt(device, AGGREGATOR_DEVICE_TYPE_ID) > 0 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false end local function device_init(driver, device) @@ -733,11 +699,6 @@ local function device_init(driver, device) if ep.endpoint_id ~= main_endpoint then local id = 0 for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == ELECTRICAL_SENSOR_ID then - for _, attr in pairs(device_type_attribute_map[ELECTRICAL_SENSOR_ID]) do - device:add_subscribed_attribute(attr) - end - end id = math.max(id, dt.device_type_id) end for _, attr in pairs(device_type_attribute_map[id] or {}) do @@ -755,31 +716,34 @@ local function device_init(driver, device) 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) - if profiling_data_still_required(device) then return end - local main_endpoint = find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local button_profile_found = initialize_buttons(driver, device, main_endpoint) - local switch_profile_found = initialize_switches(driver, device, main_endpoint) - if button_profile_found or switch_profile_found then + local profile_found = initialize_buttons_and_switches(driver, device, main_endpoint) + if device:get_field(IS_PARENT_CHILD_DEVICE) then + device:set_find_child(find_child) + end + if profile_found then return end local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local level_eps = device:get_endpoints(clusters.LevelControl.ID) + local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) local profile_name = nil - if #valve_eps > 0 then + local level_support = "" + if #level_eps > 0 then + level_support = "-level" + end + if #energy_eps > 0 and #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" + elseif #energy_eps > 0 then + profile_name = "plug" .. level_support .. "-energy-powerConsumption" + elseif #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power" + elseif #valve_eps > 0 then profile_name = "water-valve" if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then @@ -790,24 +754,6 @@ local function match_profile(driver, device) end if profile_name then device:try_update_metadata({ profile = profile_name }) - return - end - - -- after doing all previous profiling steps, attempt to re-profile main/parent switch/plug device - local electrical_tags = device:get_field(profiling_data.ELECTRICAL_TOPOLOGY).tags_on_ep[main_endpoint] - profile_name = assign_switch_profile(device, main_endpoint, false, electrical_tags) - -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since - -- these may lose fingerprinted Kelvin ranges when dynamically profiled. - if profile_name and profile_name ~= "light-level-colorTemperature" and profile_name ~= "light-color-level" then - if profile_name == "light-level" and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then - profile_name = "light-level-motion" - end - device:try_update_metadata({profile = profile_name}) - end - - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) end end @@ -1179,56 +1125,26 @@ local function occupancy_attr_handler(driver, device, ib, response) device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end -local function available_endpoints_handler(driver, device, ib, response) - local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) - local avail_eps_req_complete = true - local electrical_tags_per_ep = {} - table.sort(ib.data.elements) - for _, info in ipairs(electrical_sensor_eps or {}) do - if info.endpoint_id == ib.endpoint_id then - info.availableEndpoints = ib.data.elements - elseif info.availableEndpoints == false then -- an endpoint is found that hasn't been updated through this handler. - avail_eps_req_complete = false - end - if avail_eps_req_complete then - local electrical_tags = "" - if info.power then electrical_tags = electrical_tags .. "-power" end - if info.energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - electrical_tags_per_ep[info.availableEndpoints[1].value] = electrical_tags -- set tags on first available endpoint - end - end - if avail_eps_req_complete == false then - device:set_field(ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) - else - local topology_data = {} - topology_data.topology = clusters.PowerTopology.types.Feature.SET_TOPOLOGY - topology_data.tags_on_ep = electrical_tags_per_ep - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, topology_data) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - match_profile(driver, device) - end -end - local function cumul_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then - local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} - total_imported_energy[ib.endpoint_id] = watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) - device:emit_component_event(component, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + end end end local function per_energy_imported_handler(driver, device, ib, response) if ib.data.elements.energy then - local component = device.profile.components["main"] local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local total_imported_energy = device:get_field(TOTAL_IMPORTED_ENERGY) or {} - local latest_energy_report = total_imported_energy[ib.endpoint_id] or 0 - total_imported_energy[ib.endpoint_id] = latest_energy_report + watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, total_imported_energy, {persist = true}) - device:emit_component_event(component, capabilities.energyMeter.energy({ value = total_imported_energy[ib.endpoint_id], unit = "Wh" })) + local latest_energy_report = device:get_field(TOTAL_IMPORTED_ENERGY) or 0 + local summed_energy_report = latest_energy_report + watt_hour_value + device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) + device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) end end @@ -1279,12 +1195,14 @@ local function short_release_event_handler(driver, device, ib, response) end local function active_power_handler(driver, device, ib, response) - local component = device.profile.components["main"] if ib.data.value then local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:emit_component_event(component, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - device:emit_component_event(component, capabilities.powerMeter.power({ value = 0, unit = "W"})) + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end end end @@ -1398,22 +1316,8 @@ local function device_added(driver, device) if device.network_type == device_lib.NETWORK_TYPE_CHILD then local req = clusters.OnOff.attributes.OnOff:read(device) device:send(req) - elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then - collect_and_store_electrical_sensor_info(driver, device) - local electrical_sensor_eps = device:get_field(ELECTRICAL_SENSOR_EPS) - if #electrical_sensor_eps == 0 then - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = false, tags_on_ep = {}}) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - elseif electrical_sensor_eps[1].topology == clusters.PowerTopology.types.Feature.NODE_TOPOLOGY then - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - local electrical_tags = "" - if electrical_sensor_eps[1].power then electrical_tags = electrical_tags .. "-power" end - if electrical_sensor_eps[1].energy then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - device:set_field(profiling_data.ELECTRICAL_TOPOLOGY, {topology = clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, tags_on_ep = {[switch_eps[1]] = electrical_tags}}) - device:set_field(ELECTRICAL_SENSOR_EPS, nil) - end end + -- call device init in case init is not called after added due to device caching device_init(driver, device) end @@ -1571,9 +1475,6 @@ local matter_driver_template = { [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), }, - [clusters.PowerTopology.ID] = { - [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = available_endpoints_handler, - }, [clusters.ValveConfigurationAndControl.ID] = { [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler 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 06c726854a..830a9b99f8 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 @@ -141,7 +141,6 @@ local function test_init() end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - aqara_mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) 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 2e5aef1f2d..369689e181 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 @@ -38,8 +38,7 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, - {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -178,12 +177,10 @@ local function test_init() end end test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - - -- Test added -> doConfigure logic - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - aqara_mock_device:expect_metadata_update({ profile = "4-button" }) + test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" + test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} + test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport @@ -280,7 +277,7 @@ test.register_coroutine_test( { -- don't use "aqara_mock_children[aqara_child1_ep].id," -- because energy management is at the root endpoint. - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) } ) @@ -292,7 +289,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) } ) @@ -305,7 +302,7 @@ test.register_coroutine_test( -- This is because related variable settings are required in set_poll_report_timer_and_schedule(). test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, + aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) } ) @@ -316,8 +313,10 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { - aqara_mock_children[aqara_child1_ep].id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_39) + aqara_mock_device.id, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( + aqara_mock_device, 1, cumulative_report_val_39 + ) } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 4f88338185..e812bdfdaa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" @@ -43,7 +42,6 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -53,30 +51,10 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoint_id = 2, clusters = { { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug - } - }, - { - endpoint_id = 3, - clusters = { - { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY - }, - device_types = { - { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor - } - }, - { - endpoint_id = 4, - clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, - }, - device_types = { - { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug } }, }, @@ -102,20 +80,16 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, - { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug - { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor } }, }, }) local subscribed_attributes_periodic = { - clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } @@ -669,30 +643,9 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) - read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) - test.socket.matter:__expect_send({ mock_device.id, read_req }) - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - 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" }) - test.wait_for_events() - test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) - test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "nil 2", - profile = "plug-level-energy-powerConsumption", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", 4) - }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init } ) @@ -700,21 +653,9 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) - local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) - test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) - local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) - for i, cluster in ipairs(subscribed_attributes_periodic) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_periodic)) - end - end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.wait_for_events() - test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) + mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init_periodic } ) @@ -751,7 +692,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 1) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) } }, { 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 8d10daea11..f34f432ec7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -63,7 +63,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) 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 b211bf8f53..7d3211bdfb 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 @@ -90,7 +90,6 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 7a5c39d4c2..624a3ab205 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 @@ -122,7 +122,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) 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 3c0c32037a..139c519dbf 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -198,7 +198,6 @@ local function test_init() if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -237,7 +236,6 @@ local function test_init_mcd_unsupported_switch_device_type() end end test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - mock_device_mcd_unsupported_switch_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index d2c7da6085..a35552007a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -409,7 +409,6 @@ local function test_init_parent_child_switch_types() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) - mock_device_parent_child_switch_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -427,7 +426,6 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) - mock_device_onoff:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -440,7 +438,6 @@ end local function test_init_parent_client_child_server() local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) - mock_device_parent_client_child_server:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -449,7 +446,6 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) - mock_device_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -457,7 +453,6 @@ end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) - mock_device_color_dimmer:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -474,9 +469,7 @@ local function test_init_mounted_on_off_control() end end test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) - mock_device_mounted_on_off_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) - mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_on_off_control) end @@ -492,16 +485,13 @@ local function test_init_mounted_dimmable_load_control() end end test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) - mock_device_mounted_dimmable_load_control:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) - mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) - mock_device_water_valve:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -529,9 +519,7 @@ local function test_init_parent_child_different_types() end test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) - mock_device_parent_child_different_types:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) - mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_different_types) @@ -546,7 +534,6 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() - mock_device_parent_child_unsupported_device_type:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -575,12 +562,7 @@ local function test_init_light_level_motion() subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) end end - test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - mock_device_light_level_motion:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. - test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) - mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) - mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_light_level_motion) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 66f019cdb0..de62865597 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -180,9 +180,7 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -248,9 +246,7 @@ local function test_init_parent_child_endpoints_non_sequential() end test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - mock_device_parent_child_endpoints_non_sequential:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index a69cdfeda7..cbf8fb0afe 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -23,8 +23,6 @@ local child_profile_override = t_utils.get_profile_definition("switch-binary.yml local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 -local child3_ep = 40 -local child4_ep = 50 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", @@ -70,24 +68,6 @@ local mock_device = test.mock_device.build_test_matter_device({ {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug } }, - { - endpoint_id = child3_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - }, - { - endpoint_id = child4_ep, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - }, - device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug - } - } } }) @@ -158,9 +138,7 @@ local function test_init() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - mock_device:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device) @@ -183,22 +161,6 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 4", - profile = "plug-binary", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child3_ep) - }) - - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 5", - profile = "plug-binary", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", child4_ep) - }) end local mock_children_child_profile_override = {} @@ -221,9 +183,7 @@ local function test_init_child_profile_override() local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) - mock_device_child_profile_override:set_field("__ELECTRICAL_TOPOLOGY", {topology = false, tags_on_ep = {}}, {persist = false}) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) - mock_device_child_profile_override:expect_metadata_update({ profile = "plug-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.mock_device.add_test_device(mock_device_child_profile_override) From 4aa644b3a6f691a89c3ac250e6f28edd25287ae5 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Tue, 5 Aug 2025 10:52:25 -0500 Subject: [PATCH 062/449] Set Energy Management Endpoint field on init --- drivers/SmartThings/matter-switch/src/init.lua | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c4e9d6dceb..c8e3f47cec 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -593,7 +593,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) local parent_child_device = false local switch_eps = device:get_endpoints(clusters.OnOff.ID) table.sort(switch_eps) - for _, ep in ipairs(switch_eps) do + for idx, ep in ipairs(switch_eps) do if device:supports_server_cluster(clusters.OnOff.ID, ep) then num_switch_server_eps = num_switch_server_eps + 1 if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint @@ -610,7 +610,7 @@ local function build_child_switch_profiles(driver, device, main_endpoint) } ) parent_child_device = true - if _ == 1 and string.find(child_profile, "energy") then + if idx == 1 and string.find(child_profile, "energy") then -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) end @@ -695,8 +695,15 @@ local function device_init(driver, device) end local main_endpoint = find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for _, ep in ipairs(device.endpoints) do + for idx, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= main_endpoint then + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + local child_profile = assign_child_profile(device, ep) + if idx == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + end + end local id = 0 for _, dt in ipairs(ep.device_types) do id = math.max(id, dt.device_type_id) From c7e810367143fef16e1ec5ee9d654a1ce67196c3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 4 Aug 2025 15:58:32 -0700 Subject: [PATCH 063/449] WWSTCERT-7333 Sense by MACO | Universal --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index c5da938889..3753e8b23b 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -97,6 +97,11 @@ matterManufacturer: vendorId: 0x1547 productId: 0x03EC deviceProfileName: contact-battery + - id: "5447/1005" + deviceLabel: Sense by MACO | Universal + vendorId: 0x1547 + productId: 0x03ED + deviceProfileName: contact-battery # Meross - id: "4933/16897" deviceLabel: Smart Presence Sensor From 9b6857caa533950d03d76408da3b3d88e75c2055 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Mon, 11 Aug 2025 12:16:18 -0500 Subject: [PATCH 064/449] Fail deploy if we fail to get the existing drivers for a channel This is important because if we dont do this, the bulk upload will unassign any driver that was not a part of the changed drivers for this deploy. --- tools/deploy.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/deploy.py b/tools/deploy.py index ab64e29cde..c9d92448f5 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -79,6 +79,9 @@ print("Failed to retrieve channel's current drivers") print("Error code: "+str(response.status_code)) print("Error response: "+response.text) + # if we cant get the existing drivers in the channel the bulk upload will + # unassign drivers from the channel. Exit to fail the deploy with a fail status + exit(1) else: response_json = json.loads(response.text)["items"] for driver in response_json: @@ -95,6 +98,12 @@ "driverVersion": driver[VERSION] } ) + if driver_info_response.status_code != 200: + print("Failed to retrieve detailed driver info for {driver}") + print("Error code: " + str(driver_info_response.status_code)) + print("Error response: "+ driver_info_response.text) + exit(1) + driver_info_response_json = json.loads(driver_info_response.text)["items"][0] if PACKAGEKEY in driver_info_response_json: packageKey = driver_info_response_json[PACKAGEKEY] From 7e774c3211cf9d296fef6580bc483de27d3ad712 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 12 Aug 2025 10:26:03 -0500 Subject: [PATCH 065/449] Revert "philips-hue: Fix bug where `added` handler fails for migrated devices" This reverts commit 2533e079be45a5b4996fbc310f89d5863c2bddd4. --- .../src/handlers/lifecycle_handlers/init.lua | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua index 9dfdf48326..c9d413841d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -135,15 +135,12 @@ local valid_migration_kwargs = { "force_migrate_type" } function LifecycleHandlers.migrate_device(driver, device, ...) + local migration_kwargs = select(-1, ...) local opts = {} - local args = table.pack(...) - if #args > 0 then - local migration_kwargs = select(-1, ...) - if type(migration_kwargs) == "table" then - for _, key in ipairs(valid_migration_kwargs) do - if migration_kwargs[key] then - opts[key] = migration_kwargs[key] - end + if type(migration_kwargs) == "table" then + for _, key in ipairs(valid_migration_kwargs) do + if migration_kwargs[key] then + opts[key] = migration_kwargs[key] end end end From f143d31aec5edc43c20a4074ac04485de7b7f0ec Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 12 Aug 2025 10:26:03 -0500 Subject: [PATCH 066/449] Revert "philips-hue: Fix bug where `added` handler fails for migrated devices" This reverts commit 2533e079be45a5b4996fbc310f89d5863c2bddd4. --- .../src/handlers/lifecycle_handlers/init.lua | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua index 9dfdf48326..c9d413841d 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -135,15 +135,12 @@ local valid_migration_kwargs = { "force_migrate_type" } function LifecycleHandlers.migrate_device(driver, device, ...) + local migration_kwargs = select(-1, ...) local opts = {} - local args = table.pack(...) - if #args > 0 then - local migration_kwargs = select(-1, ...) - if type(migration_kwargs) == "table" then - for _, key in ipairs(valid_migration_kwargs) do - if migration_kwargs[key] then - opts[key] = migration_kwargs[key] - end + if type(migration_kwargs) == "table" then + for _, key in ipairs(valid_migration_kwargs) do + if migration_kwargs[key] then + opts[key] = migration_kwargs[key] end end end From c7bdb47fab72d61408d47a2974ea38ab522f3d85 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 12 Aug 2025 13:29:57 -0700 Subject: [PATCH 067/449] WWSTCERT-7459 Eve Light Switch --- drivers/SmartThings/matter-switch/fingerprints.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index ed3b9c2504..68165e95bd 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -237,14 +237,19 @@ matterManufacturer: vendorId: 0x130A productId: 0x005D deviceProfileName: switch-binary - + - id: "4874/67" + deviceLabel: Eve Light Switch + vendorId: 0x130A + productId: 0x0043 + deviceProfileName: light-binary + #Ezviz - id: "5172/4096" deviceLabel: A3 Home Gateway vendorId: 0x1434 productId: 0x1000 deviceProfileName: matter-bridge - + #GE - id: "4921/177" deviceLabel: Cync Full Color 2 Inch Wafer From b37cfaff220f02f70be94cf29322432dd46e7b21 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 18 Aug 2025 10:35:10 -0700 Subject: [PATCH 068/449] fixup: change icon for eve switch --- drivers/SmartThings/matter-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 68165e95bd..feeca749ef 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -241,7 +241,7 @@ matterManufacturer: deviceLabel: Eve Light Switch vendorId: 0x130A productId: 0x0043 - deviceProfileName: light-binary + deviceProfileName: switch-binary #Ezviz - id: "5172/4096" From 2c8c3b363f2009b20e8df163f0e4630cfbf25231 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 21 Aug 2025 11:27:09 -0700 Subject: [PATCH 069/449] WWSTCERT-7620 ULTRALOQ Bolt Smart Matter Door Lock --- 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 7ad02e6ac7..36b7d115f1 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -99,6 +99,11 @@ matterManufacturer: vendorId: 0x147F productId: 0x0001 deviceProfileName: lock-user-pin-battery + - id: "5247/8" + deviceLabel: ULTRALOQ Bolt Smart Matter Door Lock + vendorId: 0x147F + productId: 0x0008 + deviceProfileName: lock-user-pin-battery matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock From e439731cec66693adacf97c342f7305f3352e921 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 21 Aug 2025 11:41:11 -0700 Subject: [PATCH 070/449] WWSTCERT-7582 Sense by MACO | Window T&T --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3753e8b23b..8072dc6a84 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -102,6 +102,11 @@ matterManufacturer: vendorId: 0x1547 productId: 0x03ED deviceProfileName: contact-battery + - id: "5447/1002" + deviceLabel: Sense by MACO | Window TT + vendorId: 0x1547 + productId: 0x03EA + deviceProfileName: contact-battery # Meross - id: "4933/16897" deviceLabel: Smart Presence Sensor From 912f0111505875f9d2d96cc3b654ba51698b26f5 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Fri, 22 Aug 2025 08:09:19 -0500 Subject: [PATCH 071/449] matter-switch: Add profile for 9-button device https://smartthings.atlassian.net/browse/CHAD-16315 --- .../profiles/9-button-battery.yml | 63 +++++++++++++++++++ .../profiles/9-button-batteryLevel.yml | 62 ++++++++++++++++++ .../matter-switch/profiles/9-button.yml | 60 ++++++++++++++++++ .../SmartThings/matter-switch/src/init.lua | 2 +- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button-battery.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button.yml diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml new file mode 100644 index 0000000000..8aa7df8e39 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml @@ -0,0 +1,63 @@ +name: 9-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml new file mode 100644 index 0000000000..ae90de2d01 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml @@ -0,0 +1,62 @@ +name: 9-button-batteryLevel +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/9-button.yml b/drivers/SmartThings/matter-switch/profiles/9-button.yml new file mode 100644 index 0000000000..3894a40d13 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button.yml @@ -0,0 +1,60 @@ +name: 9-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 57335bf10b..a22904e517 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -289,7 +289,7 @@ local START_BUTTON_PRESS = "__start_button_press" local TIMEOUT_THRESHOLD = 10 --arbitrary timeout local HELD_THRESHOLD = 1 -- this is the number of buttons for which we have a static profile already made -local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8} +local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8, 9} -- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a -- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because From 59768f1affa6bb4ef4b072d8c554d7b7c0f377bb Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 22 Aug 2025 10:34:54 -0500 Subject: [PATCH 072/449] add support for matter hrap device types (#2283) --- drivers/SmartThings/matter-hrap/config.yml | 6 + .../SmartThings/matter-hrap/fingerprints.yml | 11 + .../network-infrastructure-manager.yml | 16 + .../profiles/thread-border-router.yml | 14 + .../client/commands/DatasetResponse.lua | 103 ++++++ .../client/commands/init.lua | 23 ++ .../src/ThreadBorderRouterManagement/init.lua | 148 ++++++++ .../attributes/ActiveDatasetTimestamp.lua | 68 ++++ .../server/attributes/BorderRouterName.lua | 69 ++++ .../server/attributes/InterfaceEnabled.lua | 69 ++++ .../server/attributes/ThreadVersion.lua | 68 ++++ .../server/attributes/init.lua | 24 ++ .../commands/GetActiveDatasetRequest.lua | 79 ++++ .../server/commands/init.lua | 28 ++ .../types/Feature.lua | 54 +++ .../types/init.lua | 15 + .../src/WiFiNetworkManagement/init.lua | 58 +++ .../server/attributes/Ssid.lua | 68 ++++ .../server/attributes/init.lua | 24 ++ .../src/embedded-cluster-utils.lua | 62 ++++ drivers/SmartThings/matter-hrap/src/init.lua | 217 +++++++++++ .../test_thread_border_router_network.lua | 342 ++++++++++++++++++ 22 files changed, 1566 insertions(+) create mode 100644 drivers/SmartThings/matter-hrap/config.yml create mode 100644 drivers/SmartThings/matter-hrap/fingerprints.yml create mode 100644 drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml create mode 100644 drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua create mode 100644 drivers/SmartThings/matter-hrap/src/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua diff --git a/drivers/SmartThings/matter-hrap/config.yml b/drivers/SmartThings/matter-hrap/config.yml new file mode 100644 index 0000000000..21d79bc8ec --- /dev/null +++ b/drivers/SmartThings/matter-hrap/config.yml @@ -0,0 +1,6 @@ +name: 'Matter HRAP' +packageKey: 'matter-hrap' +permissions: + matter: {} +description: "SmartThings driver for Matter HRAP devices" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/matter-hrap/fingerprints.yml b/drivers/SmartThings/matter-hrap/fingerprints.yml new file mode 100644 index 0000000000..7b768709c2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/fingerprints.yml @@ -0,0 +1,11 @@ +matterGeneric: + - id: "matter/network-manager" + deviceLabel: Matter Network Infrastructure Manager + deviceTypes: + - id: 0x0090 # NIM + deviceProfileName: network-infrastructure-manager + - id: "matter/thread-border-router" + deviceLabel: Matter Thread Border Router + deviceTypes: + - id: 0x0091 # TBR + deviceProfileName: thread-border-router diff --git a/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml new file mode 100644 index 0000000000..725676c502 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml @@ -0,0 +1,16 @@ +name: network-infrastructure-manager +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: wifiInformation + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml new file mode 100644 index 0000000000..b879dd2e76 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml @@ -0,0 +1,14 @@ +name: thread-border-router +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua new file mode 100644 index 0000000000..0cf8facc53 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua @@ -0,0 +1,103 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DatasetResponse = {} + +DatasetResponse.NAME = "DatasetResponse" +DatasetResponse.ID = 0x0002 +DatasetResponse.field_defs = { + { + name = "dataset", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.OctetString1", + }, +} + +function DatasetResponse:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function DatasetResponse:build_test_command_response(device, endpoint_id, dataset, interaction_status) + local function init(self, device, endpoint_id, dataset) + local out = {} + local args = {dataset} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = DatasetResponse, + __tostring = DatasetResponse.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + false, + true + ) + end + local self_request = init(self, device, endpoint_id, dataset) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + self_request.info_blocks[1].tlv, + interaction_status + ) +end + +function DatasetResponse:init() + return nil +end + +function DatasetResponse:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DatasetResponse:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(DatasetResponse, {__call = DatasetResponse.init}) + +return DatasetResponse diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua new file mode 100644 index 0000000000..3c8bee49fa --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.client.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementClientCommands = {} + +function ThreadBorderRouterManagementClientCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementClientCommands, command_mt) + +return ThreadBorderRouterManagementClientCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua new file mode 100644 index 0000000000..3b3485d03d --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua @@ -0,0 +1,148 @@ +local cluster_base = require "st.matter.cluster_base" +local ThreadBorderRouterManagementServerAttributes = require "ThreadBorderRouterManagement.server.attributes" +local ThreadBorderRouterManagementServerCommands = require "ThreadBorderRouterManagement.server.commands" +local ThreadBorderRouterManagementClientCommands = require "ThreadBorderRouterManagement.client.commands" +local ThreadBorderRouterManagementTypes = require "ThreadBorderRouterManagement.types" + +local ThreadBorderRouterManagement = {} + +ThreadBorderRouterManagement.ID = 0x0452 +ThreadBorderRouterManagement.NAME = "ThreadBorderRouterManagement" +ThreadBorderRouterManagement.server = {} +ThreadBorderRouterManagement.client = {} +ThreadBorderRouterManagement.server.attributes = ThreadBorderRouterManagementServerAttributes:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.server.commands = ThreadBorderRouterManagementServerCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.client.commands = ThreadBorderRouterManagementClientCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.types = ThreadBorderRouterManagementTypes + +function ThreadBorderRouterManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "BorderRouterName", + [0x0001] = "BorderAgentID", + [0x0002] = "ThreadVersion", + [0x0003] = "InterfaceEnabled", + [0x0004] = "ActiveDatasetTimestamp", + [0x0005] = "PendingDatasetTimestamp", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ThreadBorderRouterManagement:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "GetActiveDatasetRequest", + [0x0001] = "GetPendingDatasetRequest", + [0x0003] = "SetActiveDatasetRequest", + [0x0004] = "SetPendingDatasetRequest", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +function ThreadBorderRouterManagement:get_client_command_by_id(command_id) + local client_id_map = { + [0x0002] = "DatasetResponse", + } + if client_id_map[command_id] ~= nil then + return self.client.commands[client_id_map[command_id]] + end + return nil +end + +ThreadBorderRouterManagement.attribute_direction_map = { + ["BorderRouterName"] = "server", + ["BorderAgentID"] = "server", + ["ThreadVersion"] = "server", + ["InterfaceEnabled"] = "server", + ["ActiveDatasetTimestamp"] = "server", + ["PendingDatasetTimestamp"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.attribute_direction_map[alias] = "server" + end + end +end + +ThreadBorderRouterManagement.command_direction_map = { + ["GetActiveDatasetRequest"] = "server", + ["GetPendingDatasetRequest"] = "server", + ["SetActiveDatasetRequest"] = "server", + ["SetPendingDatasetRequest"] = "server", + ["DatasetResponse"] = "client", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "server" + end + end +end + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.client.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "client" + end + end +end + +ThreadBorderRouterManagement.FeatureMap = ThreadBorderRouterManagement.types.Feature + +function ThreadBorderRouterManagement.are_features_supported(feature, feature_map) + if (ThreadBorderRouterManagement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].attributes[key] +end +ThreadBorderRouterManagement.attributes = {} +setmetatable(ThreadBorderRouterManagement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].commands[key] +end +ThreadBorderRouterManagement.commands = {} +setmetatable(ThreadBorderRouterManagement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ThreadBorderRouterManagement.server.events[key] +end +ThreadBorderRouterManagement.events = {} +setmetatable(ThreadBorderRouterManagement.events, event_helper_mt) + +setmetatable(ThreadBorderRouterManagement, {__index = cluster_base}) + +return ThreadBorderRouterManagement + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua new file mode 100644 index 0000000000..b0dc67b590 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActiveDatasetTimestamp = { + ID = 0x0004, + NAME = "ActiveDatasetTimestamp", + base_type = require "st.matter.data_types.Uint64", +} + +function ActiveDatasetTimestamp:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActiveDatasetTimestamp:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActiveDatasetTimestamp: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 ActiveDatasetTimestamp:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActiveDatasetTimestamp, {__call = ActiveDatasetTimestamp.new_value, __index = ActiveDatasetTimestamp.base_type}) +return ActiveDatasetTimestamp + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua new file mode 100644 index 0000000000..33fd11c111 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local BorderRouterName = { + ID = 0x0000, + NAME = "BorderRouterName", + base_type = require "st.matter.data_types.UTF8String1", +} + +function BorderRouterName:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function BorderRouterName:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function BorderRouterName:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function BorderRouterName:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function BorderRouterName: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 BorderRouterName:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(BorderRouterName, {__call = BorderRouterName.new_value, __index = BorderRouterName.base_type}) +return BorderRouterName + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua new file mode 100644 index 0000000000..c2676bfaf0 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local InterfaceEnabled = { + ID = 0x0003, + NAME = "InterfaceEnabled", + base_type = require "st.matter.data_types.Boolean", +} + +function InterfaceEnabled:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function InterfaceEnabled:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function InterfaceEnabled:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function InterfaceEnabled:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function InterfaceEnabled: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 InterfaceEnabled:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(InterfaceEnabled, {__call = InterfaceEnabled.new_value, __index = InterfaceEnabled.base_type}) +return InterfaceEnabled + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua new file mode 100644 index 0000000000..6630c863e9 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ThreadVersion = { + ID = 0x0002, + NAME = "ThreadVersion", + base_type = require "st.matter.data_types.Uint16", +} + +function ThreadVersion:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ThreadVersion:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ThreadVersion: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 ThreadVersion:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ThreadVersion, {__call = ThreadVersion.new_value, __index = ThreadVersion.base_type}) +return ThreadVersion + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua new file mode 100644 index 0000000000..96cad47fdd --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +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("ThreadBorderRouterManagement.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 ThreadBorderRouterManagementServerAttributes = {} + +function ThreadBorderRouterManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerAttributes, attr_mt) + +return ThreadBorderRouterManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua new file mode 100644 index 0000000000..b63982575e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GetActiveDatasetRequest = {} + +GetActiveDatasetRequest.NAME = "GetActiveDatasetRequest" +GetActiveDatasetRequest.ID = 0x0000 +GetActiveDatasetRequest.field_defs = { +} + +function GetActiveDatasetRequest:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GetActiveDatasetRequest, + __tostring = GetActiveDatasetRequest.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function GetActiveDatasetRequest:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GetActiveDatasetRequest:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function GetActiveDatasetRequest:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(GetActiveDatasetRequest, {__call = GetActiveDatasetRequest.init}) + +return GetActiveDatasetRequest diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua new file mode 100644 index 0000000000..ec75068d8e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua @@ -0,0 +1,28 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementServerCommands = {} + +function ThreadBorderRouterManagementServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerCommands, command_mt) + +local status, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") +if status then + aliases:add_to_class(ThreadBorderRouterManagementServerCommands) +end + +return ThreadBorderRouterManagementServerCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua new file mode 100644 index 0000000000..c8116cd481 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua @@ -0,0 +1,54 @@ +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.PAN_CHANGE = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + PAN_CHANGE = 0x0001, +} + +Feature.is_pan_change_set = function(self) + return (self.value & self.PAN_CHANGE) ~= 0 +end + +Feature.set_pan_change = function(self) + if self.value ~= nil then + self.value = self.value | self.PAN_CHANGE + else + self.value = self.PAN_CHANGE + end +end + +Feature.unset_pan_change = function(self) + self.value = self.value & (~self.PAN_CHANGE & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.PAN_CHANGE + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_pan_change_set = Feature.is_pan_change_set, + set_pan_change = Feature.set_pan_change, + unset_pan_change = Feature.unset_pan_change, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua new file mode 100644 index 0000000000..3aece4eef2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua @@ -0,0 +1,15 @@ +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("ThreadBorderRouterManagement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ThreadBorderRouterManagementTypes = {} + +setmetatable(ThreadBorderRouterManagementTypes, types_mt) + +return ThreadBorderRouterManagementTypes + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua new file mode 100644 index 0000000000..061ee5854f --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua @@ -0,0 +1,58 @@ +local cluster_base = require "st.matter.cluster_base" +local WiFiNetworkManagementServerAttributes = require "WiFiNetworkManagement.server.attributes" + +local WiFiNetworkManagement = {} + +WiFiNetworkManagement.ID = 0x0451 +WiFiNetworkManagement.NAME = "WiFiNetworkManagement" +WiFiNetworkManagement.server = {} +WiFiNetworkManagement.client = {} +WiFiNetworkManagement.server.attributes = WiFiNetworkManagementServerAttributes:set_parent_cluster(WiFiNetworkManagement) + +function WiFiNetworkManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Ssid", + [0x0001] = "PassphraseSurrogate", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +WiFiNetworkManagement.attribute_direction_map = { + ["Ssid"] = "server", + ["PassphraseSurrogate"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.WiFiNetworkManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + WiFiNetworkManagement.attribute_direction_map[alias] = "server" + end + end +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = WiFiNetworkManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, WiFiNetworkManagement.NAME)) + end + return WiFiNetworkManagement[direction].attributes[key] +end +WiFiNetworkManagement.attributes = {} +setmetatable(WiFiNetworkManagement.attributes, attribute_helper_mt) + +setmetatable(WiFiNetworkManagement, {__index = cluster_base}) + +return WiFiNetworkManagement + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua new file mode 100644 index 0000000000..e7a2a43fb5 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Ssid = { + ID = 0x0000, + NAME = "Ssid", + base_type = require "st.matter.data_types.OctetString1", +} + +function Ssid:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Ssid:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Ssid: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 Ssid:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Ssid, {__call = Ssid.new_value, __index = Ssid.base_type}) +return Ssid + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua new file mode 100644 index 0000000000..de0b55c8b1 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +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("WiFiNetworkManagement.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 WiFiNetworkManagementServerAttributes = {} + +function WiFiNetworkManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(WiFiNetworkManagementServerAttributes, attr_mt) + +return WiFiNetworkManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua new file mode 100644 index 0000000000..ca36cd7562 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua @@ -0,0 +1,62 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_api_13 = { + [clusters.ThreadBorderRouterManagement.ID] = clusters.ThreadBorderRouterManagement, +} + +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 < 13 and embedded_clusters_api_13[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_13[cluster_id] + if not opts then opts = {} end + 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-hrap/src/init.lua b/drivers/SmartThings/matter-hrap/src/init.lua new file mode 100644 index 0000000000..df05e7ce83 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/init.lua @@ -0,0 +1,217 @@ +-- Copyright 2025 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 MatterDriver = require "st.matter.driver" +local data_types = require "st.matter.data_types" +local im = require "st.matter.interaction_model" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local lustre_utils = require "lustre.utils" +local st_utils = require "st.utils" +local version = require "version" +local log = require "log" + +local CURRENT_ACTIVE_DATASET_TIMESTAMP = "__CURRENT_ACTIVE_DATASET_TIMESTAMP" +local GET_ACTIVE_DATASET_RETRY_ATTEMPTS = "__GET_ACTIVE_DATASET_RETRY_ATTEMPTS" + +-- Include driver-side definitions when lua libs api version is <13 +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" + clusters.WiFiNetworkManagement = require "WiFiNetworkManagement" +end + + +--[[ ATTRIBUTE HANDLERS ]]-- + +local function border_router_name_attribute_handler(driver, device, ib) + -- per the spec, the recommended attribute format is ._meshcop._udp. This logic removes the MeshCoP suffix IFF it is present + local meshCop_name = ib.data.value + local terminal_display_char = (string.find(meshCop_name, "._meshcop._udp") or 64) - 1 -- where 64-1=63, the maximum allowed length for BorderRouterName + local display_name = string.sub(meshCop_name, 1, terminal_display_char) + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.borderRouterName({ value = display_name })) +end + +local function ssid_attribute_handler(driver, device, ib) + if (ib.data.value == string.char(data_types.Null.ID) or ib.data.value == nil) then -- Matter TLV-encoded NULL or Lua-encoded NULL + device.log.info("No primary Wi-Fi network is available") + return + end + local valid_utf8, utf8_err = lustre_utils.validate_utf8(ib.data.value) + if valid_utf8 then + device:emit_event_for_endpoint(ib.endpoint, capabilities.wifiInformation.ssid({ value = ib.data.value })) + else + device.log.info("UTF-8 validation of SSID failed: Error: '"..utf8_err.."'") + end +end + +local function thread_interface_enabled_attribute_handler(driver, device, ib) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("enabled")) + else + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("disabled")) + end +end + +-- Spec uses TLV encoding of Thread Version, which should be mapped to a more human-readable name +local VERSION_TLV_MAP = { + [1] = "1.0.0", + [2] = "1.1.0", + [3] = "1.2.0", + [4] = "1.3.0", + [5] = "1.4.0", +} + +local function thread_version_attribute_handler(driver, device, ib) + local version_name = VERSION_TLV_MAP[ib.data.value] + if version_name then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadVersion({ value = version_name })) + else + device.log.warn("The received TLV-encoded Thread version does not have a provided mapping to a human-readable version format") + end +end + +local function active_dataset_timestamp_handler(driver, device, ib) + if not ib.data.value then + device.log.info("No Thread operational dataset configured") + elseif ib.data.value ~= device:get_field(CURRENT_ACTIVE_DATASET_TIMESTAMP) then + device:send(clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(device, ib.endpoint_id)) + device:set_field(CURRENT_ACTIVE_DATASET_TIMESTAMP, ib.data.value, { persist = true }) + end +end + + +--[[ COMMAND HANDLERS ]]-- + +local threadNetwork = capabilities.threadNetwork +local TLV_TYPE_ATTR_MAP = { + [0] = threadNetwork.channel, + [1] = threadNetwork.panId, + [2] = threadNetwork.extendedPanId, + [3] = threadNetwork.networkName, + -- [4] intentionally omitted, refers to the MeshCoP PSKc + [5] = threadNetwork.networkKey, +} + +local function dataset_response_handler(driver, device, ib) + if not ib.info_block.data.elements.dataset then + device.log.debug_with({ hub_logs = true }, "Received empty Thread operational dataset") + return + end + + local operational_dataset_length = ib.info_block.data.elements.dataset.byte_length + local spec_defined_max_dataset_length = 254 + if operational_dataset_length > spec_defined_max_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset is too long") + return + end + + -- parse dataset + local operational_dataset = ib.info_block.data.elements.dataset.value + local cur_byte = 1 + while cur_byte + 1 <= operational_dataset_length do + local tlv_type = string.byte(operational_dataset, cur_byte) + local tlv_length = string.byte(operational_dataset, cur_byte + 1) + if (cur_byte + 1 + tlv_length) > operational_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset has a malformed TLV encoding") + return + end + local tlv_mapped_attr = TLV_TYPE_ATTR_MAP[tlv_type] + if tlv_mapped_attr then + -- extract the value from a TLV-encoded message. Message format: byte tag + byte length + length byte value + local tlv_value = operational_dataset:sub(cur_byte + 2, cur_byte + 1 + tlv_length) + -- format data as required by threadNetwork attribute properties + if tlv_mapped_attr == threadNetwork.channel or tlv_mapped_attr == threadNetwork.panId then + tlv_value = st_utils.deserialize_int(tlv_value, tlv_length) + elseif tlv_mapped_attr ~= threadNetwork.networkName then + tlv_value = st_utils.bytes_to_hex_string(tlv_value) + end + device:emit_event(tlv_mapped_attr({ value = tlv_value })) + end + cur_byte = cur_byte + 2 + tlv_length + end +end + +local function get_active_dataset_response_handler(driver, device, ib) + if ib.status == im.InteractionResponse.Status.FAILURE then + -- per spec, on a GetActiveDatasetRequest failure, a failure response is sent over the same command. + -- on failure, retry the read up to 3 times before failing out. + local retries_attempted = device:get_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS) or 0 + if retries_attempted < 3 then + device.log.error_with({ hub_logs = true }, "Failed to retrieve Thread operational dataset. Retrying " .. retries_attempted + 1 .. "/3") + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, retries_attempted + 1) + else + -- do not retry again, but reset the count to 0. + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, 0) + end + elseif ib.status == im.InteractionResponse.Status.UNSUPPORTED_ACCESS then + device.log.error_with({ hub_logs = true }, + "Failed to retrieve Thread operational dataset, since the GetActiveDatasetRequest command was not executed over CASE" + ) + end +end + + +--[[ LIFECYCLE HANDLERS ]]-- + +local function device_init(driver, device) + device:subscribe() +end + + +--[[ MATTER DRIVER TEMPLATE ]]-- + +local matter_driver_template = { + lifecycle_handlers = { + init = device_init, + }, + matter_handlers = { + attr = { + [clusters.WiFiNetworkManagement.ID] = { + [clusters.WiFiNetworkManagement.attributes.Ssid.ID] = ssid_attribute_handler, + }, + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.attributes.BorderRouterName.ID] = border_router_name_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ThreadVersion.ID] = thread_version_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled.ID] = thread_interface_enabled_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp.ID] = active_dataset_timestamp_handler, + } + }, + cmd_response = { + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse.ID] = dataset_response_handler, + [clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest.ID] = get_active_dataset_response_handler, + } + } + }, + subscribed_attributes = { + [capabilities.threadBorderRouter.ID] = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + }, + [capabilities.wifiInformation.ID] = { + clusters.WiFiNetworkManagement.attributes.Ssid, + } + }, + supported_capabilities = { + capabilities.threadBorderRouter, + capabilities.threadNetwork, + capabilities.wifiInformation, + } +} + +local matter_driver = MatterDriver("matter-hrap", matter_driver_template) +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() 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 new file mode 100644 index 0000000000..0467e19de3 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua @@ -0,0 +1,342 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local data_types = require "st.matter.data_types" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +clusters.WifiNetworkMangement = require "WiFiNetworkManagement" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("network-infrastructure-manager.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + 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.ThreadBorderRouterManagement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.WifiNetworkMangement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0090, device_type_revision = 1,} -- Network Infrastructure Manager + } + } + } +}) + +local cluster_subscribe_list = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + clusters.WifiNetworkMangement.attributes.Ssid, +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + 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( + "ThreadVersion should display the correct stringified version", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 3 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.2.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 4 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.3.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 5 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.4.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 6 + ) + }) + end +) + +test.register_message_test( + "InterfaceEnabled should correctly display enabled or disabled", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("enabled")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("disabled")) + } + } +) + +test.register_message_test( + "BorderRouterName should correctly display the given name", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "jane bar._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "jane bar"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo no suffix") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo no suffix"})) + }, + } +) + +test.register_message_test( + "wifiInformation capability should correctly display the Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, "test name for ssid!") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.wifiInformation.ssid({ value = "test name for ssid!" })) + } + } +) + +test.register_message_test( + "Null-valued ssid (TLV 0x14) should correctly fail", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(data_types.Null.ID)) + } + } + } +) + +test.register_message_test( + "Ssid inputs using non-UTF8 encoding should not display an Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(0xC0)) -- 0xC0 never appears in utf8 + } + } + } +) + +local hex_dataset = [[ +0E 08 00 00 68 87 D0 B2 00 00 00 03 00 00 18 35 +06 00 04 00 1F FF C0 02 08 25 31 25 A9 B2 16 7F +35 07 08 FD 6E D1 57 02 B4 CD BF 05 10 33 AF 36 +F8 13 8E 8F F9 50 6D 67 22 9B FD F2 40 03 0D 53 +54 2D 35 30 33 32 30 30 31 31 39 36 01 02 D9 78 +04 10 E2 29 D8 2A 84 B2 7D A1 AC 8D D8 71 64 AC +66 7F 0C 04 02 A0 FF F8 +]] + +local serializable_hex_dataset = hex_dataset:gsub("%s+", ""):gsub("..", function(cc) + return string.char(tonumber(cc, 16)) +end) + +test.register_coroutine_test( + "Thread DatasetResponse parsing should emit the correct capability events on an ActiveDatasetTimestamp update. Else, nothing should happen", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + test.wait_for_events() + + -- after some amount of time, a device init occurs or we re-subscribe for other reasons. + -- Since no change to the ActiveDatasetTimestamp has occurred, no re-read should occur + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.wait_for_events() + + -- after some more amount of time, a device init occurs or we re-subscribe for other reasons. + -- This time, their ActiveDatasetTimestamp has updated, so we should re-read the operational dataset. + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 2 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + end +) + +test.run_registered_tests() From a8c7cb534eb4505cb7485923be0297b73e24e219 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Fri, 18 Apr 2025 13:24:24 -0500 Subject: [PATCH 073/449] Fix unit tests for new features in 58 Lua libs release New features include: * Init messages being generated by the hub instead of in the lua libs * Modular profiles * More native handler registration by default --- .../src/test/test_cook_top.lua | 19 +- .../src/test/test_dishwasher.lua | 8 +- .../src/test/test_extractor_hood.lua | 16 +- .../src/test/test_laundry_dryer.lua | 5 +- .../src/test/test_laundry_washer.lua | 6 +- .../src/test/test_matter_appliance_rpc_5.lua | 108 ++-- .../src/test/test_microwave_oven.lua | 8 +- .../matter-appliance/src/test/test_oven.lua | 8 +- .../src/test/test_refrigerator.lua | 6 +- .../test/test_matter_button_parent_child.lua | 1 + .../src/test/test_matter_multi_button.lua | 26 +- .../src/test/test_battery_storage.lua | 9 +- .../matter-energy/src/test/test_evse.lua | 11 +- .../src/test/test_evse_energy_meas.lua | 11 +- .../src/test/test_solar_power.lua | 10 +- .../src/test/test_aqara_matter_lock.lua | 17 +- .../src/test/test_bridged_matter_lock.lua | 90 +-- .../matter-lock/src/test/test_matter_lock.lua | 11 +- .../src/test/test_matter_lock_battery.lua | 24 +- .../test/test_matter_lock_batteryLevel.lua | 39 +- .../src/test/test_matter_lock_codes.lua | 28 +- .../src/test/test_matter_lock_cota.lua | 21 +- .../src/test/test_matter_lock_unlatch.lua | 26 +- .../src/test/test_new_matter_lock.lua | 26 +- .../src/test/test_new_matter_lock_battery.lua | 200 ++----- .../src/test/test_matter_media_speaker.lua | 555 +++++++++--------- .../test/test_matter_media_video_player.lua | 135 +++-- drivers/SmartThings/matter-rvc/src/init.lua | 9 +- .../matter-rvc/src/test/test_matter_rvc.lua | 11 +- .../test/test_matter_air_quality_sensor.lua | 3 + .../src/test/test_matter_flow_sensor.lua | 3 +- .../test/test_matter_freeze_leak_sensor.lua | 95 ++- .../src/test/test_matter_pressure_sensor.lua | 12 +- .../src/test/test_matter_rain_sensor.lua | 18 +- .../src/test/test_matter_sensor.lua | 5 +- .../src/test/test_matter_sensor_battery.lua | 9 +- .../test/test_matter_sensor_featuremap.lua | 20 +- .../src/test/test_matter_sensor_rpc.lua | 2 - .../src/test/test_matter_smoke_co_alarm.lua | 7 +- .../test_matter_smoke_co_alarm_battery.lua | 4 +- .../test/test_aqara_climate_sensor_w100.lua | 13 +- .../src/test/test_aqara_light_switch_h2.lua | 22 +- .../src/test/test_electrical_sensor.lua | 12 +- .../src/test/test_matter_button.lua | 11 +- .../src/test/test_matter_light_fan.lua | 9 +- .../src/test/test_matter_multi_button.lua | 29 +- .../test_matter_multi_button_switch_mcd.lua | 119 ++-- .../test/test_matter_switch_device_types.lua | 74 ++- .../test_multi_switch_parent_child_lights.lua | 18 +- .../test_multi_switch_parent_child_plugs.lua | 17 +- .../src/test/test_third_reality_mk1.lua | 12 +- .../src/test/test_matter_air_purifier.lua | 1 + .../test/test_matter_air_purifier_modular.lua | 61 +- .../src/test/test_matter_heat_pump.lua | 26 +- .../src/test/test_matter_room_ac.lua | 40 +- .../src/test/test_matter_room_ac_modular.lua | 90 +-- .../src/test/test_matter_thermo_battery.lua | 2 + .../test/test_matter_thermo_featuremap.lua | 3 +- ...st_matter_thermo_multiple_device_types.lua | 119 ++-- .../src/test/test_matter_thermostat.lua | 33 +- ...est_matter_thermostat_composed_bridged.lua | 37 +- .../test/test_matter_thermostat_modular.lua | 40 +- .../src/test/test_matter_thermostat_rpc5.lua | 145 +++++ .../src/test/test_matter_water_heater.lua | 52 +- .../src/test/test_matter_window_covering.lua | 92 ++- .../test/test_frient_contact_sensor_pro.lua | 8 + .../src/test/test_zigbee_contact.lua | 8 + .../src/test/test_zigbee_contact_tyco.lua | 9 + .../zigbee-humidity-sensor/src/init.lua | 2 +- .../src/test/test_aqara_sensor.lua | 9 + .../src/test/test_humidity_plaid_systems.lua | 9 + .../src/test/test_humidity_temperature.lua | 9 + .../test_humidity_temperature_battery.lua | 9 + .../test/test_humidity_temperature_sensor.lua | 8 + .../test_all_capabilities_zigbee_motion.lua | 9 + .../test/test_frient_motion_sensor_pro.lua | 8 + .../src/test/test_zigbee_power_meter.lua | 4 +- ..._zigbee_power_meter_consumption_report.lua | 2 +- .../src/test/test_frient_smoke_detector.lua | 8 + .../src/test/test_zigbee_sound_sensor.lua | 8 + .../test/test_all_capability_zigbee_bulb.lua | 13 +- .../src/test/test_frient_switch.lua | 8 + .../src/test/test_multi_switch_power.lua | 16 + .../test/test_robb_smarrt_2-wire_dimmer.lua | 8 + .../src/test/test_robb_smarrt_knob_dimmer.lua | 8 + .../src/test/test_switch_power.lua | 4 +- .../src/test/test_zigbee_ezex_switch.lua | 8 + .../src/test/test_zigbee_thermostat.lua | 2 +- .../test_centralite_water_leak_sensor.lua | 8 + .../test/test_frient_water_leak_sensor.lua | 8 + .../test/test_samjin_water_leak_sensor.lua | 8 + .../src/test/test_sinope_zigbee_water.lua | 8 + .../test_smartthings_water_leak_sensor.lua | 8 + .../src/test/test_zigbee_water.lua | 8 + .../src/fibaro-flood-sensor/init.lua | 12 - .../test/test_fibaro_door_window_sensor_1.lua | 16 + ...ro_door_window_sensor_with_temperature.lua | 16 + .../src/test/test_fibaro_flood_sensor.lua | 8 + .../src/test/test_fibaro_motion_sensor.lua | 16 + .../src/test/test_generic_sensor.lua | 36 +- .../test_smartthings_water_leak_sensor.lua | 16 + .../src/test/test_aeotec_dimmer_switch.lua | 8 + .../src/test/test_aeotec_nano_dimmer.lua | 8 + .../test/test_fibaro_walli_double_switch.lua | 16 + .../src/test/test_multichannel_device.lua | 8 + .../src/test/test_qubino_din_dimmer.lua | 8 + .../src/test/test_qubino_flush_2_relay.lua | 16 + .../src/test/test_qubino_flush_dimmer.lua | 8 + ...t_qubino_temperature_sensor_with_power.lua | 8 + .../src/test/test_zooz_double_plug.lua | 16 + .../test/test_zwave_dimmer_power_energy.lua | 8 + .../test/test_zwave_switch_electric_meter.lua | 8 + .../test/test_zwave_switch_energy_meter.lua | 8 + .../test/test_zwave_switch_power_meter.lua | 8 + .../test/test_aeotec_radiator_thermostat.lua | 8 + .../src/test/test_fibaro_heat_controller.lua | 8 + .../test/test_popp_radiator_thermostat.lua | 8 + .../src/test/test_qubino_flush_thermostat.lua | 8 + .../src/test/test_zwave_thermostat.lua | 8 + .../src/test/test_fibaro_roller_shutter.lua | 2 +- .../src/test/test_qubino_flush_shutter.lua | 2 +- .../test_zwave_iblinds_window_treatment.lua | 8 +- .../test_zwave_springs_window_treatment.lua | 2 +- .../src/test/test_zwave_window_treatment.lua | 2 +- 124 files changed, 2038 insertions(+), 1190 deletions(-) create mode 100644 drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua 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 1fffd78597..9aa45f7951 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -70,34 +70,29 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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 = "cook-surface-one-tl-cook-surface-two-tl" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Verify device profile update", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "cook-surface-one-tl-cook-surface-two-tl" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - test.register_coroutine_test( "Assert component to endpoint map", function() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua index f35a88c012..c6923c3de4 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua @@ -57,6 +57,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -71,7 +73,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -79,12 +80,13 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ profile = "dishwasher-tn-tl" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end 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 8d02396191..91339b78ca 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -86,6 +85,8 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.fanMode.ID] = { clusters.FanControl.attributes.FanModeSequence, @@ -117,16 +118,16 @@ local function test_init() end end end - - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) 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 = "extractor-hood-hepa-ac-wind" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_onoff() + test.disable_startup_messages() local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, @@ -135,15 +136,16 @@ local function test_init_onoff() clusters.FanControl.attributes.WindSetting, clusters.OnOff.attributes.OnOff } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_onoff) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(mock_device_onoff)) end end - test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.mock_device.add_test_device(mock_device_onoff) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) + test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure"}) mock_device_onoff:expect_metadata_update({ profile = "extractor-hood-wind-light" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 7695c51d48..10e0de0f5e 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -56,6 +56,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -76,8 +78,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 966d1f7d17..0334bf61cb 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua @@ -56,6 +56,8 @@ local mock_device_washer = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_washer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -69,7 +71,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -77,8 +78,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_washer) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 57ec49cfbf..8f9a8c81d8 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 @@ -268,6 +268,8 @@ local mock_device_refrigerator = test.mock_device.build_test_matter_device({ }) local function test_init_dishwasher() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_dishwasher) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -282,7 +284,6 @@ local function test_init_dishwasher() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dishwasher) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -290,11 +291,21 @@ local function test_init_dishwasher() end end test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_dishwasher) test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dishwasher.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "doConfigure"}) + mock_device_dishwasher:expect_metadata_update({ profile = "dishwasher-tn-tl" }) + mock_device_dishwasher:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -local function test_init_washer_dryer() +local function test_init_dryer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_dryer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -308,29 +319,62 @@ local function test_init_washer_dryer() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dryer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_dryer)) end end - local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dryer.id, read_req}) + mock_device_dryer:expect_metadata_update({ profile = "laundry-dryer-tn-tl" }) + mock_device_dryer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_washer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_washer) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LaundryWasherMode.attributes.CurrentMode, + clusters.LaundryWasherMode.attributes.SupportedModes, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.TemperatureControl.attributes.TemperatureSetpoint, + clusters.TemperatureControl.attributes.MaxTemperature, + clusters.TemperatureControl.attributes.MinTemperature, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request_washer:merge(cluster:subscribe(mock_device_washer)) + subscribe_request:merge(cluster:subscribe(mock_device_washer)) end end - test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_dryer) - test.mock_device.add_test_device(mock_device_washer) - test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_washer.id, read_req}) + mock_device_washer:expect_metadata_update({ profile = "laundry-washer-tn-tl" }) + mock_device_washer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_oven() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_oven) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -342,7 +386,6 @@ local function test_init_oven() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_oven) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -350,12 +393,16 @@ local function test_init_oven() end end test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_oven) test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "init" }) + test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "doConfigure"}) + mock_device_oven:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_refrigerator() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_refrigerator) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -365,7 +412,6 @@ local function test_init_refrigerator() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_refrigerator) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -373,14 +419,19 @@ local function test_init_refrigerator() end end test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_refrigerator) test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "init" }) + test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_refrigerator.id, read_req}) + mock_device_refrigerator:expect_metadata_update({ profile = "refrigerator-freezer-tn-tl" }) + mock_device_refrigerator:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -420,7 +471,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -460,7 +510,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -495,12 +544,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 28 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -535,12 +583,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 50 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -575,12 +622,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -615,12 +661,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -660,7 +705,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -700,7 +744,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -740,7 +783,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -780,7 +822,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -820,7 +861,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, 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 3d7008b036..3c2e1fbc51 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -54,6 +54,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OperationalState.attributes.OperationalState, clusters.OperationalState.attributes.OperationalError, @@ -63,17 +65,19 @@ local function test_init() clusters.MicrowaveOvenControl.attributes.MaxCookTime, clusters.MicrowaveOvenControl.attributes.CookTime } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster: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.matter:__expect_send({ mock_device.id, clusters.MicrowaveOvenControl.attributes.MaxCookTime:read( mock_device, APPLICATION_ENDPOINT) }) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function init_supported_microwave_oven_modes() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua index 6af4556c6a..58c38e078c 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -112,6 +112,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -123,7 +125,6 @@ local function test_init() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -131,8 +132,11 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua index 1ef5734d0f..3dc19750f3 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua @@ -67,6 +67,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -76,7 +78,6 @@ local function test_init() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -84,8 +85,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 7655c29e1e..a9d2629803 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 @@ -74,6 +74,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ } local function test_init() + test.set_rpc_version(0) 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 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 42d97c109d..2ecf083ba7 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 @@ -73,35 +73,47 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added results in a profile update + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + mock_device:expect_metadata_update({ profile = "4-button-battery" }) + + -- init results in subscription interaction 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - mock_device:expect_metadata_update({ profile = "4-button-battery" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioing state to provisioned + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + -- simulate the profile change update taking affect and the device info changing local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "4-buttons-battery" local device_info_json = dkjson.encode(device_info_copy) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) end test.set_test_init_function(test_init) +-- this one is failing because it expects added or test.register_message_test( "Handle single press sequence, no hold", { { 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 453858d7c2..27f8585185 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -62,6 +62,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, @@ -69,16 +71,15 @@ local function test_init() clusters.PowerSource.attributes.BatPercentRemaining, clusters.PowerSource.attributes.BatChargeState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.matter:__expect_send({ mock_device.id, @@ -90,6 +91,8 @@ local function test_init() clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse.lua b/drivers/SmartThings/matter-energy/src/test/test_evse.lua index ad9db31150..6add71bde9 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse.lua @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -90,19 +92,22 @@ local function test_init() clusters.DeviceEnergyManagementMode.attributes.CurrentMode, clusters.DeviceEnergyManagementMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-power-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 91332241b7..84c431ffe2 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 @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -89,16 +91,15 @@ local function test_init() clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.matter:__expect_send({ mock_device.id, @@ -107,6 +108,10 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-energy-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 049b364d6f..87fde17a23 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -71,21 +71,22 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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 }) local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) @@ -93,6 +94,9 @@ local function test_init() mock_device.id, read_req }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 b64324d8f7..7427ad31d0 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 @@ -41,7 +41,8 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 0x0001, --u32 bitmap + feature_map = clusters.DoorLock.types.Feature.PIN_CREDENTIAL | + clusters.DoorLock.types.Feature.USER } }, device_types = { @@ -52,6 +53,13 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) @@ -63,7 +71,12 @@ local function test_init() subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm: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.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" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 3c95216dc5..41ecbd612e 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 @@ -15,6 +15,7 @@ local test = require "integration_test" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local mock_device_record = { @@ -41,58 +42,75 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local mock_device_record_level = { - profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), - manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus - endpoints = { - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } + profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), + manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, - }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, }, }, + }, } local mock_device_level = test.mock_device.build_test_matter_device(mock_device_record_level) local function test_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.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes("[]", {visibility = {displayed = false}})) + ) + local req = clusters.DoorLock.attributes.MaxPINCodeLength:read(mock_device, 10) + req:merge(clusters.DoorLock.attributes.MinPINCodeLength:read(mock_device, 10)) + req:merge(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device, 10)) + 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) + - local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) - test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) - test.mock_device.add_test_device(mock_device_level) + 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) + test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) end test.set_test_init_function(test_init) test.register_coroutine_test( - "doConfigure lifecycle event for base-lock-nobattery", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for base-lock-nobattery", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.register_coroutine_test( - "doConfigure lifecycle event for Level Lock Plus profile", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) - mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for Level Lock Plus profile", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) + mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.run_registered_tests() 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 7701d5a90c..0123d43239 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -43,12 +43,21 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 6aff133939..92af7fcabc 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 @@ -15,6 +15,7 @@ local test = require "integration_test" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" @@ -67,28 +68,41 @@ local mock_device_no_battery_record = { local mock_device_no_battery = test.mock_device.build_test_matter_device(mock_device_no_battery_record) local function test_init() + test.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + 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.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) local function test_init_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_no_battery) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "added" }) + test.socket.capability:__expect_send( + mock_device_no_battery:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + mock_device_no_battery:expect_metadata_update({ profile = "lock-without-codes-nobattery" }) + 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)) + 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.mock_device.add_test_device(mock_device_no_battery) test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "doConfigure" }) mock_device_no_battery:expect_metadata_update({ profile = "base-lock-nobattery" }) mock_device_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 22f9ccd29a..d76556a42d 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 @@ -44,10 +44,15 @@ local mock_device = test.mock_device.build_test_matter_device(mock_device_record local function test_init() - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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)) + 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" }) end test.set_test_init_function(test_init) @@ -84,20 +89,20 @@ test.register_message_test( message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.warning()), }, { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( - mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK + ), }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + }, } ) 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 c672410296..ad68ffbe3b 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 @@ -12,20 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- Copyright 2022 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 capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") @@ -35,6 +21,7 @@ local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock local im = require "st.matter.interaction_model" local types = DoorLock.types + local mock_device_record = { profile = t_utils.get_profile_definition("base-lock.yml"), manufacturer_info = {vendor_id = 0xcccc, product_id = 0x1}, @@ -56,7 +43,7 @@ local mock_device_record = { cluster_type = "SERVER", feature_map = 0x0101, -- PIN & USR }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, }, }, @@ -64,13 +51,18 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -189,11 +181,11 @@ local function init_code_slot(slot_number, name, device) ) local credential = DoorLock.types.DlCredential( - { + { credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = slot_number, } - ) + ) test.socket.matter:__expect_send( { device.id, 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 6303ec7bcd..f25fe2a19c 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 @@ -12,20 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- Copyright 2022 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 capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") @@ -64,6 +50,9 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + 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)) @@ -71,7 +60,9 @@ local function test_init() 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) 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 2e390f4d34..92adc6c6fd 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 @@ -54,28 +54,28 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "lock-unlatch" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-unlatch" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() 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 9659c9484d..442593b188 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 @@ -55,6 +55,13 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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)) @@ -68,23 +75,16 @@ local function test_init() 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-user-pin" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() 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 603f056c06..d8657ccae3 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 @@ -174,24 +174,55 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.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.mock_device.add_test_device(mock_device) + 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}})) + ) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_unlatch() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) test.socket["matter"]:__expect_send({mock_device_unlatch.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_user_pin) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) @@ -203,10 +234,22 @@ local function test_init_user_pin() subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) test.socket["matter"]:__expect_send({mock_device_user_pin.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_user_pin) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin_schedule_unlatch() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) @@ -220,7 +263,12 @@ local function test_init_user_pin_schedule_unlatch() subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) test.socket["matter"]:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -228,18 +276,6 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Test lock profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -264,18 +300,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -301,18 +325,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -339,18 +351,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -376,18 +376,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -414,18 +402,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -453,18 +429,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -490,18 +454,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -528,18 +480,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -567,18 +507,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -604,18 +532,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -642,18 +558,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, 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 5444652b29..cf5bc44648 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -49,286 +48,283 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) - local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel - } - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device) + subscribe_request:merge(clusters.LevelControl.attributes.CurrentLevel: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" }) end test.set_test_init_function(test_init) test.register_message_test( - "Mute and unmute commands should send the appropriate commands", + "Mute and unmute commands should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "mute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "unmute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "mute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "unmute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) + } + } ) test.register_message_test( - "Set mute command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - } + "Set mute command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } } + } ) test.register_message_test( - "Set volume command should send the appropriate commands", + "Set volume command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) } + } ) test.register_message_test( - "Volume up/down command should send the appropriate commands", + "Volume up/down command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - -- volume up - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) - }, - -- volume down - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + -- volume up + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) + }, + -- volume down + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + } ) local function refresh_commands(dev) @@ -338,25 +334,24 @@ local function refresh_commands(dev) end test.register_message_test( - "Handle received refresh.", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - refresh_commands(mock_device) - } - }, - } + "Handle received refresh.", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + refresh_commands(mock_device) + } + }, + } ) - 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 52cde71ed3..ebae93ca53 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -84,34 +83,88 @@ local mock_device_variable_speed = test.mock_device.build_test_matter_device({ } }) +local supported_key_codes = { + "UP", + "DOWN", + "LEFT", + "RIGHT", + "SELECT", + "BACK", + "EXIT", + "MENU", + "SETTINGS", + "HOME", + "NUMBER0", + "NUMBER1", + "NUMBER2", + "NUMBER3", + "NUMBER4", + "NUMBER5", + "NUMBER6", + "NUMBER7", + "NUMBER8", + "NUMBER9" +} local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.MediaPlayback.attributes.CurrentState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device)) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) + + test.mock_device.add_test_device(mock_device_variable_speed) + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "init" }) subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_variable_speed) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_variable_speed)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device_variable_speed)) test.socket.matter:__expect_send({mock_device_variable_speed.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_variable_speed) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "doConfigure" }) + mock_device_variable_speed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop", "rewind", "fastForward"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) end test.set_test_init_function(test_init) @@ -557,28 +610,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) @@ -608,28 +640,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_variable_speed:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index c8fae5aaad..4165535fef 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -16,9 +16,6 @@ local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local area_type = require "Global.types.AreaTypeTag" -local landmark = require "Global.types.LandmarkTag" - local embedded_cluster_utils = require "embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 10 @@ -512,15 +509,15 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp if location_info.location_name.value ~= "" then area_name = location_info.location_name.value elseif location_info.floor_number.value ~= nil and location_info.area_type.value ~= nil then - area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) elseif location_info.floor_number.value ~= nil then area_name = location_info.floor_number.value .. "F" elseif location_info.area_type.value ~= nil then - area_name = upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) end end if area_name == "" then - area_name = upper_to_camelcase(string.gsub(landmark.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.LandmarkTag.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) end table.insert(supported_areas, {["areaId"] = area_id, ["areaName"] = area_name}) end 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 1fb2afb752..5bed191802 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -64,6 +64,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.mode.ID] = { clusters.RvcRunMode.attributes.SupportedModes, @@ -90,9 +92,14 @@ local function test_init() end end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) 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 = "rvc-clean-mode-service-area" }) + test.socket.matter:__expect_send({mock_device.id, clusters.RvcOperationalState.attributes.AcceptedCommandList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 b2b8ae4ebf..871aec64e2 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 @@ -249,6 +249,9 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at test.mock_device.add_test_device(generic_mock_device) end +-- TODO add tests for configuration using modular profiles +test.set_rpc_version(7) + local function test_init() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { 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 880ed55c89..f6312dfc8f 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 @@ -53,6 +53,7 @@ local subscribed_attributes = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then @@ -60,8 +61,6 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) end test.set_test_init_function(test_init) 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 46bc88a771..7aae047a29 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 @@ -19,73 +19,62 @@ local clusters = require "st.matter.clusters" clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" local mock_device_freeze_leak = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } }, - 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.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector - } + device_types = { + {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector - } + device_types = { + {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector } } + } }) -local subscribed_attributes = { - clusters.BooleanState.attributes.StateValue, - clusters.BooleanStateConfiguration.attributes.SensorFault, -} - local function test_init_freeze_leak() - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_freeze_leak) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_freeze_leak)) - end - end + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_freeze_leak) + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "init" }) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 1)}) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 2)}) + local subscribe_request = clusters.BooleanState.attributes.StateValue:subscribe(mock_device_freeze_leak) + subscribe_request:merge(clusters.BooleanStateConfiguration.attributes.SensorFault:subscribe(mock_device_freeze_leak)) test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_freeze_leak) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) + mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) + mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_freeze_leak) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) - mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) - mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_freeze_leak } -) - test.register_message_test( "Boolean state freeze detection reports should generate correct messages", { 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 27d3b90842..68134ed677 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 @@ -49,17 +49,11 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoints = matter_endpoints }) -local function subscribe_on_init(dev) - local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - return subscribe_request -end - local function test_init() - test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device since we are just testing the "pressure-battery" profile specifically - mock_device:set_field("__battery_checked", 1, {persist = true}) + local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) 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 b23f72de53..835de811ae 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 @@ -55,28 +55,24 @@ local subscribed_attributes = { } local function test_init_rain() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_rain) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_rain) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_rain)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "init" }) test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) test.socket.matter:__expect_send({mock_device_rain.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_rain) + + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) + mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) + mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_rain) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) - mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) - mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_rain } -) - test.register_message_test( "Boolean state rain detection reports should generate correct messages", { 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 5bed934846..7116a03b92 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua @@ -118,7 +118,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - test.set_rpc_version(5) end test.set_test_init_function(test_init) @@ -134,8 +133,10 @@ local function subscribe_on_init_presence_sensor(dev) end local function test_init_presence_sensor() - test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_presence_sensor) + test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "init" }) + test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_presence_sensor.id, read_attribute_list}) 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 48b72e232c..72484efffe 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 @@ -56,20 +56,21 @@ local cluster_subscribe_list_humidity_battery = { } local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 91d795d885..ed9718b947 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 @@ -125,52 +125,52 @@ local cluster_subscribe_list_temp_humidity = { } local function test_init_humidity_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) - mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) + mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_humidity_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_no_battery) local subscribe_request_humidity_no_battery = cluster_subscribe_list_humidity_no_battery[1]:subscribe(mock_device_humidity_no_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_no_battery) do if i > 1 then subscribe_request_humidity_no_battery:merge(cluster:subscribe(mock_device_humidity_no_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_no_battery.id, subscribe_request_humidity_no_battery}) - test.mock_device.add_test_device(mock_device_humidity_no_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "doConfigure" }) mock_device_humidity_no_battery:expect_metadata_update({ profile = "humidity" }) mock_device_humidity_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_temp_humidity() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_temp_humidity) local subscribe_request_temp_humidity = cluster_subscribe_list_temp_humidity[1]:subscribe(mock_device_temp_humidity) for i, cluster in ipairs(cluster_subscribe_list_temp_humidity) do if i > 1 then subscribe_request_temp_humidity:merge(cluster:subscribe(mock_device_temp_humidity)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "init" }) test.socket.matter:__expect_send({mock_device_temp_humidity.id, subscribe_request_temp_humidity}) - test.mock_device.add_test_device(mock_device_temp_humidity) - test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "doConfigure" }) mock_device_temp_humidity:expect_metadata_update({ profile = "temperature-humidity" }) mock_device_temp_humidity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 d8b38e392a..7eb31d5c29 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 @@ -57,8 +57,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device because we are using the catch-all "sensor.yml" profile just for testing - mock_device:set_field("__battery_checked", 1, {persist = true}) test.set_rpc_version(3) end test.set_test_init_function(test_init) 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 f5029147d7..86060ab442 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 @@ -27,6 +27,7 @@ end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("smoke-co-temp-humidity-comeas.yml"), + _provisioning_state = "TYPED", -- we want this to have doConfigure on startup manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -73,6 +74,10 @@ local cluster_subscribe_list = { } local function test_init() + -- The startup messages are enabled, so this device will get an init, + -- and doConfigure (because provisioning_state is TYPED on the device). + test.mock_device.add_test_device(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -80,11 +85,9 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) 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 9bcdc54e44..b618b3e5d6 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 @@ -72,18 +72,20 @@ local cluster_subscribe_list = { } 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, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + 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" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) 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 830a9b99f8..d2e99b2331 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 @@ -18,7 +18,6 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button @@ -121,6 +120,8 @@ local function configure_buttons() end local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -140,17 +141,17 @@ local function test_init() end end + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) configure_buttons() aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) - test.set_rpc_version(5) - - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) device_info_copy.profile.id = "3-button-battery-temperature-humidity" 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 369689e181..29544d0e8d 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 @@ -17,12 +17,9 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" - local aqara_parent_ep = 4 local aqara_child1_ep = 1 local aqara_child2_ep = 2 @@ -159,7 +156,8 @@ local function configure_buttons() end local function test_init() - local opts = { persist = true } + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.Switch.server.events.InitialPress, @@ -176,13 +174,16 @@ local function test_init() subscribe_request:merge(cluster:subscribe(aqara_mock_device)) end end + + -- Test added -> doConfigure logic + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" - test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} - test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} + configure_buttons() + aqara_mock_device:expect_metadata_update({ profile = "4-button" }) aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") @@ -206,11 +207,6 @@ local function test_init() parent_assigned_child_key = string.format("%d", aqara_child2_ep) }) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - configure_buttons() - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - - aqara_mock_device:set_field(DEFERRED_CONFIGURE, true, opts) local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) device_info_copy.profile.id = "4-button" local device_info_json = dkjson.encode(device_info_copy) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index bf791d906b..d9759b7add 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -14,12 +14,14 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local version = require "version" -clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" -clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), @@ -56,7 +58,7 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug } - }, + } }, }) 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 f34f432ec7..23023b3d02 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -58,19 +58,24 @@ local function configure_buttons() end 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 + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) configure_buttons() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "buttons-battery" local device_info_json = dkjson.encode(device_info_copy) 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 7d3211bdfb..078b99c7fe 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 @@ -84,12 +84,19 @@ local CLUSTER_SUBSCRIBE_LIST ={ } 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 + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + + 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 = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 624a3ab205..d8b0116101 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 @@ -99,7 +99,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) @@ -116,28 +116,39 @@ local function configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init 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.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" }) - test.mock_device.add_test_device(mock_device) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "5-buttons-battery" local device_info_json = dkjson.encode(device_info_copy) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() + expect_configure_buttons() end - test.set_test_init_function(test_init) test.register_message_test( 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 139c519dbf..af7511100e 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 @@ -181,7 +181,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) @@ -192,23 +192,29 @@ local function configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + test.mock_device.add_test_device(mock_child) + + -- added sets a bunch of fields on the device, and calls init 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() - test.mock_device.add_test_device(mock_device) - test.mock_device.add_test_device(mock_child) mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -216,37 +222,30 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", mock_device_ep5) }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - configure_buttons() + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + + test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) + mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init_mcd_unsupported_switch_device_type() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) - end - end - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - mock_device_mcd_unsupported_switch_device_type:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "switch-binary", - parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, - parent_assigned_child_key = string.format("%d", 7) - }) - test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) -- make sure the cache is populated end test.set_test_init_function(test_init) @@ -404,7 +403,49 @@ test.register_message_test( test.register_coroutine_test( "Test MCD configuration not including switch for unsupported switch device type, create child device instead", function() - end, + -- added sets a bunch of fields on the device, and calls init + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) + end + end + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "added" }) + test.wait_for_events() + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "init" }) + test.wait_for_events() + + -- doConfigure sets the provisioning state to provisioned + mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) + mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + mock_device_mcd_unsupported_switch_device_type:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "switch-binary", + parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, + parent_assigned_child_key = string.format("%d", 7) + }) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) + test.wait_for_events() + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device_mcd_unsupported_switch_device_type.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + end, { test_init = test_init_mcd_unsupported_switch_device_type } ) @@ -413,7 +454,7 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) - configure_buttons() + expect_configure_buttons() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index a35552007a..0046244bcd 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -1,8 +1,23 @@ +-- Copyright 2025 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 test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +test.disable_startup_messages() + local mock_device_onoff = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { @@ -406,15 +421,19 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device }) local function test_init_parent_child_switch_types() + test.mock_device.add_test_device(mock_device_parent_child_switch_types) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_switch_types) - mock_device_parent_child_switch_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -426,6 +445,8 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -436,12 +457,18 @@ local function test_init_onoff_client() end local function test_init_parent_client_child_server() + test.mock_device.add_test_device(mock_device_parent_client_child_server) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_client_child_server) end local function test_init_dimmer() @@ -453,12 +480,15 @@ end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_mounted_on_off_control() + test.mock_device.add_test_device(mock_device_mounted_on_off_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } @@ -468,13 +498,18 @@ local function test_init_mounted_on_off_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_on_off_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) + test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_on_off_control) end local function test_init_mounted_dimmable_load_control() + test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } @@ -484,20 +519,27 @@ local function test_init_mounted_dimmable_load_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_dimmable_load_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) + test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_child_different_types() + test.mock_device.add_test_device(mock_device_parent_child_different_types) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -517,13 +559,15 @@ local function test_init_parent_child_different_types() subscribe_request:merge(cluster:subscribe(mock_device_parent_child_different_types)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_different_types) - mock_device_parent_child_different_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -534,10 +578,12 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() + test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) mock_device_parent_child_unsupported_device_type:expect_device_create({ type = "EDGE_CHILD", @@ -549,6 +595,7 @@ local function test_init_parent_child_unsupported_device_type() end local function test_init_light_level_motion() + test.mock_device.add_test_device(mock_device_light_level_motion) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -562,8 +609,15 @@ local function test_init_light_level_motion() subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "added" }) test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_light_level_motion) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) + test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index de62865597..21c9e1087d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -15,8 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" + +test.disable_startup_messages() + local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 local OPTIONS_OVERRIDE = 0x01 @@ -159,6 +161,7 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -178,12 +181,16 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -225,6 +232,7 @@ for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endp end local function test_init_parent_child_endpoints_non_sequential() + test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -244,12 +252,16 @@ local function test_init_parent_child_endpoints_non_sequential() subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index cbf8fb0afe..03c898b7f5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -15,9 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" +test.disable_startup_messages() + local child_profile = t_utils.get_profile_definition("plug-binary.yml") local child_profile_override = t_utils.get_profile_definition("switch-binary.yml") local parent_ep = 10 @@ -132,16 +133,21 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -177,16 +183,21 @@ for i, endpoint in ipairs(mock_device_child_profile_override.endpoints) do end local function test_init_child_profile_override() + test.mock_device.add_test_device(mock_device_child_profile_override) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) + + test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "added" }) + test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "init" }) test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_child_profile_override) for _, child in pairs(mock_children_child_profile_override) do test.mock_device.add_test_device(child) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 398d3756c8..31f99f537d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -201,6 +201,8 @@ local function configure_buttons() 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 } @@ -208,13 +210,17 @@ local function test_init() 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() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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) 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 f05c5ee175..d9cb8c6309 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 @@ -12,6 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" 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 c21de741e2..e5f44e3289 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 @@ -11,15 +11,19 @@ -- 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 test = require "integration_test" -test.set_rpc_version(8) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" +test.disable_startup_messages() + if version.api < 10 then clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" @@ -224,6 +228,21 @@ local cluster_subscribe_list_configured = { } local function test_init_basic() + test.mock_device.add_test_device(mock_device_basic) + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -231,10 +250,25 @@ local function test_init_basic() end end test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_basic) end local function test_init_ap_thermo_aqs_preconfigured() + test.mock_device.add_test_device(mock_device_ap_thermo_aqs) + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.AttributeList, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_ap_thermo_aqs)) + end + test.socket.matter:__expect_send({ mock_device_ap_thermo_aqs.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "init" }) local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do for _, attribute in ipairs(attributes) do @@ -246,7 +280,6 @@ local function test_init_ap_thermo_aqs_preconfigured() end end test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_ap_thermo_aqs) end local expected_update_metadata= { @@ -283,11 +316,16 @@ local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "doConfigure" }) + test.socket.matter:__queue_receive({ + mock_device_basic.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_basic, 1, {uint32(0)}) + }) mock_device_basic:expect_metadata_update(expected_update_metadata) mock_device_basic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_basic.raw_st_data) device_info_copy.profile.id = "air-purifier-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -339,7 +377,6 @@ local expected_update_metadata= { local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do - print("Adding attribute to subscribe", attributes) for _, attribute in ipairs(attributes) do if subscribe_request == nil then subscribe_request = attribute:subscribe(mock_device_ap_thermo_aqs) @@ -352,11 +389,17 @@ end test.register_coroutine_test( "Test profile change on init for AP and Thermo and AQS combined device type", function() - mock_device_ap_thermo_aqs:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_ap_thermo_aqs:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "doConfigure" }) - mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) mock_device_ap_thermo_aqs:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_ap_thermo_aqs.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_ap_thermo_aqs, 1, {uint32(0)}) + }) + mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) + + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_ap_thermo_aqs.raw_st_data) device_info_copy.profile.id = "air-purifier-modular" local device_info_json = dkjson.encode(device_info_copy) 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 3a33e94251..9af8c51982 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 @@ -83,6 +83,8 @@ local device_desc = { } local test_init_common = function(device) + test.disable_startup_messages() + test.mock_device.add_test_device(device) local cluster_subscribe_list = { clusters.Thermostat.attributes.SystemMode, clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -107,7 +109,6 @@ local test_init_common = function(device) subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({ device.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ device.id, "added" }) local read_request_on_added = { clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -124,7 +125,8 @@ local test_init_common = function(device) device.id, read_request }) - test.mock_device.add_test_device(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ device.id, subscribe_request }) end local mock_device = test.mock_device.build_test_matter_device(device_desc) @@ -181,6 +183,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -194,6 +201,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 23*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -213,6 +225,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 39*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -226,6 +243,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 19*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", 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 6c30fae787..cec0cb9ffc 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 @@ -12,10 +12,10 @@ -- See the License for the specific language governing permissions and -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -35,15 +35,15 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, } + } } }) @@ -64,18 +64,18 @@ local mock_device_configure = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) 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 096ce71221..9be85cd84b 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 @@ -16,9 +16,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" +test.disable_startup_messages() test.set_rpc_version(8) local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -38,18 +40,18 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) @@ -70,22 +72,23 @@ local mock_device_no_state = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) + test.mock_device.add_test_device(generic_mock_device) local subscribe_request = nil for _, attributes in pairs(generic_subscribed_attributes) do for _, attribute in ipairs(attributes) do @@ -97,12 +100,27 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end +local function read_req_on_added(device) + local attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(attributes) do + read_request:merge(clus:read(device)) + end + test.socket.matter:__expect_send({ device.id, read_request }) +end + local subscribe_request_basic local function test_init_basic() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -145,6 +163,8 @@ local function test_init_basic() clusters.FanControl.attributes.WindSetting }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + read_req_on_added(mock_device_basic) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() test.socket.matter:__expect_send({mock_device_basic.id, read_setpoint_deadband}) @@ -204,8 +224,8 @@ for _, attributes in pairs(subscribed_attributes_no_state) do end end - local function test_init_no_state() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -249,6 +269,8 @@ local function test_init_no_state() }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_no_state.id, "added" }) + read_req_on_added(mock_device_no_state) -- initially, device onboards WITH thermostatOperatingState, the test below will -- check if it is removed correctly when switching to modular profile. This is done -- to test that cases where the modular profile is different from the static profile @@ -260,10 +282,16 @@ local function test_init_no_state() end -- run the profile configuration tests -local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) +local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request, thermostat_attr_list_value) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {thermostat_attr_list_value}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) device_info_copy.profile.id = "room-air-conditioner-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -292,9 +320,7 @@ local expected_metadata_basic= { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - basic", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", true) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic) + test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic, uint32(0x29)) end, { test_init = test_init_basic } ) @@ -319,9 +345,7 @@ local expected_metadata_no_state = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - no thermo state", function() - mock_device_no_state:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_no_state:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state) + test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state, uint32(0)) end, { test_init = test_init_no_state } ) 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 96fdf02f3d..35f312c67e 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 @@ -17,6 +17,8 @@ local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-batteryLevel.yml"), manufacturer_info = { 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 59ae9a433f..37f73d0e50 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 @@ -15,9 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), manufacturer_info = { 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 d1a70c3f06..2086036be5 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 @@ -14,8 +14,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +local dkjson = require "dkjson" +local uint32 = require "st.matter.data_types.Uint32" +local utils = require "st.utils" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -135,32 +137,18 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, } -local cluster_subscribe_list_disorder_endpoints = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, - clusters.Thermostat.attributes.ControlSequenceOfOperation, - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.FanModeSequence, -} - -local function test_init() - mock_device:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do +local function get_subscribe_request(device, attribute_list) + local subscribe_request = attribute_list[1]:subscribe(device) + for i, cluster in ipairs(attribute_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + return subscribe_request +end + +local function test_init() + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -171,19 +159,14 @@ local function test_init() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, get_subscribe_request(mock_device, cluster_subscribe_list)}) end test.set_test_init_function(test_init) local function test_init_disorder_endpoints() - mock_device_disorder_endpoints:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request_disorder_endpoints = cluster_subscribe_list_disorder_endpoints[1]:subscribe(mock_device_disorder_endpoints) - for i, cluster in ipairs(cluster_subscribe_list_disorder_endpoints) do - if i > 1 then - subscribe_request_disorder_endpoints:merge(cluster:subscribe(mock_device_disorder_endpoints)) - end - end - test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, subscribe_request_disorder_endpoints}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_disorder_endpoints) test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "added" }) @@ -194,25 +177,79 @@ local function test_init_disorder_endpoints() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "init" }) + test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, get_subscribe_request( + mock_device_disorder_endpoints, cluster_subscribe_list)}) end +-- run the profile configuration tests +local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) + test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) + generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + + test.wait_for_events() + + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) + device_info_copy.profile.id = "thermostat-modular" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) +end + +local expected_metadata = { + optional_component_capabilities={ + { + "main", + { + "relativeHumidityMeasurement", + "fanMode", + "fanOscillationMode", + "thermostatHeatingSetpoint", + "thermostatCoolingSetpoint" + }, + }, + }, + profile="thermostat-modular", +} + +local new_cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.RockSupport, -- These two attributes will be subscribed to following the profile + clusters.FanControl.attributes.RockSetting, -- change since the fanOscillationMode capability will be enabled. +} + test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support", function() - mock_device:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device, expected_metadata, + get_subscribe_request(mock_device, new_cluster_subscribe_list)) end ) test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support with disorder endpoints", function() - mock_device_disorder_endpoints:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device_disorder_endpoints.id, "doConfigure"}) - mock_device_disorder_endpoints:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device_disorder_endpoints:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device_disorder_endpoints, expected_metadata, + get_subscribe_request(mock_device_disorder_endpoints, new_cluster_subscribe_list)) end, { test_init = test_init_disorder_endpoints } ) 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 767b21a36e..44ed395797 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local clusters = require "st.matter.clusters" @@ -219,6 +218,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -238,6 +242,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -703,28 +712,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { 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 d8146257ef..e901ac59a4 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 @@ -15,8 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -157,6 +155,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -177,6 +181,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -513,29 +523,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 3, - utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { 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 5e44701d97..9c42c03ddb 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 @@ -14,12 +14,13 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local dkjson = require "dkjson" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" +local utils = require "st.utils" -test.set_rpc_version(8) +test.disable_startup_messages() local mock_device_basic = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -49,10 +50,10 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, device_types = { - {device_type_id = 0x0301, device_type_revision = 1}, -- Thermostat + {device_type_id = 0x0301, device_type_revision = 1} -- Thermostat } } } @@ -67,12 +68,12 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end local subscribe_request_basic local function test_init() + test.mock_device.add_test_device(mock_device_basic) local subscribed_attributes = { clusters.Thermostat.attributes.LocalTemperature, clusters.Thermostat.attributes.OccupiedCoolingSetpoint, @@ -92,14 +93,35 @@ local function test_init() clusters.FanControl.attributes.FanModeSequence, clusters.PowerSource.attributes.BatPercentRemaining, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) end -- run the profile configuration tests local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) device_info_copy.profile.id = "thermostat-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -125,8 +147,6 @@ local expected_metadata = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test_thermostat_device_type_update_modular_profile(mock_device_basic, expected_metadata, subscribe_request_basic) end, { test_init = test_init } 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 new file mode 100644 index 0000000000..b52b08027e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua @@ -0,0 +1,145 @@ +-- Copyright 2022 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 clusters = require "st.matter.clusters" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" + +-- Temperature values are converted to Celsius by the hub before reaching the driver for rpc > 5. +-- This test file is meant to verify that the driver converts Fahrenheit values > 40 degrees from F to C for rpc <= 5. +test.set_rpc_version(5) + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + 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.FanControl.ID, cluster_type = "SERVER"}, + { + cluster_id = clusters.Thermostat.ID, + cluster_revision=5, + cluster_type="SERVER", + feature_map=3, -- Heat and Cool features + }, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.PowerSource.attributes.BatPercentRemaining, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + 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_message_test( + "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((90 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 41 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((41 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 35 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 35 * 100) + } + } + } +) + +test.run_registered_tests() 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 ee8365c30c..0dbeff9796 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local version = require "version" local clusters = require "st.matter.clusters" @@ -106,29 +105,6 @@ local function test_init() end test.set_test_init_function(test_init) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, - utils.round((90 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Heating setpoint reports should generate correct messages", { @@ -140,6 +116,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 70*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -170,28 +152,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 100 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, utils.round((100 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Ensure WaterHeaderMode supportedModes are registered and setting Oven mode should send appropriate commands", { 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 2d7e3946ef..4c1eab7443 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 @@ -17,8 +17,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" + local WindowCovering = clusters.WindowCovering +test.disable_startup_messages() + local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), @@ -36,44 +39,12 @@ local mock_device = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters - { - cluster_id = clusters.WindowCovering.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 3, - }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} - }, - }, - }, - } -) - -local mock_device_switch_to_battery = test.mock_device.build_test_matter_device( - { - profile = t_utils.get_profile_definition("window-covering.yml"), - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, - endpoints = { - { - endpoint_id = 2, clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } - }, - { - endpoint_id = 10, - clusters = { -- list the clusters { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 1, + feature_map = 3, }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} @@ -100,7 +71,7 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters + clusters = { { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", @@ -130,34 +101,45 @@ local CLUSTER_SUBSCRIBE_LIST_NO_BATTERY = { } local function test_init() + 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.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) -end -local function test_init_switch_to_battery() - local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_switch_to_battery) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_switch_to_battery)) end - end - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_switch_to_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_to_battery.id, "doConfigure" }) - mock_device_switch_to_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end local function test_init_mains_powered() + test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "added" }) + test.socket.capability:__expect_send( + mock_device_mains_powered:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "init" }) local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_mains_powered) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_mains_powered)) end end test.socket.matter:__expect_send({mock_device_mains_powered.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "doConfigure" }) mock_device_mains_powered:expect_metadata_update({ profile = "window-covering" }) mock_device_mains_powered:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -794,13 +776,12 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(12)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(12)}) } ) - mock_device_switch_to_battery:expect_metadata_update({ profile = "window-covering-battery" }) - end, - { test_init = test_init_switch_to_battery } + mock_device:expect_metadata_update({ profile = "window-covering-tilt-battery" }) + end ) test.register_coroutine_test( @@ -808,12 +789,11 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(10)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(10)}) } ) - end, - { test_init = test_init_switch_to_battery } + end ) test.register_coroutine_test( 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 19673a958a..d9671a5283 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 @@ -199,6 +199,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 b01e88e62b..d7076ec9ee 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua @@ -147,6 +147,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 79e5c68fa5..e288d3a927 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 @@ -80,6 +80,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -97,6 +105,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index cf136ae1fd..31dc36ce7b 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -90,6 +90,6 @@ local zigbee_humidity_driver = { health_check = false, } -defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities) +defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities, {native_capability_attrs_enabled = true}) local driver = ZigbeeDriver("zigbee-humidity-sensor", zigbee_humidity_driver) driver:run() 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 104c36c3e5..8f414975a1 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 @@ -167,6 +167,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -186,6 +194,7 @@ test.register_coroutine_test( ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 9334c03045..d9745bb681 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 @@ -116,6 +116,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -156,6 +164,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 ca121baa9b..f3e7831b1f 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,6 +48,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -151,6 +159,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { 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 f3d30ab0c8..9afd1c3a1d 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,6 +60,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -171,6 +179,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { 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 7da190b523..d4f3c09a05 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,6 +55,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 003c7d95bf..b016503f44 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 @@ -143,6 +143,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -407,6 +415,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 753850e38e..cda87474fa 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 @@ -156,6 +156,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 afe97fb0f9..710469054f 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 @@ -99,7 +99,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -113,7 +113,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua index dcbb1b0b00..700eab4300 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua @@ -145,7 +145,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, 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 b8a685bd9a..ff4fa07184 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 @@ -270,6 +270,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 83717569f0..4ab2f31e92 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 @@ -267,6 +267,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 0301c0228c..319066fa3b 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 @@ -202,6 +202,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -340,7 +348,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -354,7 +362,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -552,6 +560,7 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:build_test_attr_report(mock_device, 556)}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + mock_device:expect_native_attr_handler_registration("colorTemperature", "colorTemperature") end ) 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 8fb1704f38..8b9b26ba13 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua @@ -222,6 +222,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2.7, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 4605e8bf8b..c9e16aadad 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 @@ -329,6 +329,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -348,6 +356,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 3e3eb5722a..c1be129215 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 @@ -105,6 +105,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 34cbff59fa..0c66abd359 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 @@ -105,6 +105,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 4115546f55..74598ef3a5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -72,7 +72,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) } ) test.socket.zigbee:__expect_send( @@ -90,7 +90,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) } ) 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 28460999f2..88470b5489 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 @@ -118,6 +118,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.766, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 bdb2b681cb..01f1a0ba74 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua @@ -120,7 +120,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) - } + }, } ) 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 fd6403d1cd..4ee674199c 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 @@ -247,6 +247,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 1d3480ef99..13bdb14268 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 @@ -253,6 +253,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 154413af45..43543e3127 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 @@ -215,6 +215,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7b85749b29..bc476f06bb 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 @@ -135,6 +135,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 8896cbba36..e4ffe5567c 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 @@ -213,6 +213,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 8ad7bef789..ab0163ef5c 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 @@ -141,6 +141,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index a407322212..144be985ae 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -22,7 +22,6 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 --- @type st.zwave.CommandClass.SensorBinary local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) --- @type st.zwave.CommandClass.SensorMultilevel -local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) local preferences = require "preferences" local configurations = require "configurations" @@ -73,14 +72,6 @@ local function sensor_binary_report_handler(self, device, cmd) device:emit_event(event) end -local function sensor_multilevel_report_handler(self, device, cmd) - if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then - local scale = 'C' - if (cmd.args.scale == SensorMultilevel.scale.temperature.FAHRENHEIT) then scale = 'F' end - device:emit_event(capabilities.temperatureMeasurement.temperature({value = cmd.args.sensor_value, unit = scale})) - end -end - local function do_configure(driver, device) configurations.initial_configuration(driver, device) -- The flood sensor can be hardwired, so update any preferences @@ -101,9 +92,6 @@ local fibaro_flood_sensor = { [cc.SENSOR_BINARY] = { [SensorBinary.REPORT] = sensor_binary_report_handler }, - [cc.SENSOR_MULTILEVEL] = { - [SensorMultilevel.REPORT] = sensor_multilevel_report_handler - } }, lifecycle_handlers = { doConfigure = do_configure 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 4cd8d0e905..fbea573d09 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 @@ -344,6 +344,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -363,6 +371,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 b100af6f1a..2b27d66868 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 @@ -232,6 +232,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -251,6 +259,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 909beb6b1d..7a9743a69e 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 @@ -130,6 +130,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25, unit = 'C'})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 25a7b6ee83..24a7f31eaf 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 @@ -312,6 +312,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -331,6 +339,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 aa7d1ff60c..643c69dd1f 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -173,7 +173,7 @@ test.register_message_test( ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (C) should be handled as temperature", { { channel = "zwave", @@ -188,12 +188,20 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 30, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (F) should be handled as temperature", { { channel = "zwave", @@ -208,6 +216,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70, unit = "F" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -266,6 +282,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -286,6 +310,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 e97c97029b..2321b5bd09 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 @@ -200,6 +200,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -220,6 +228,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 37, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 f172ba9fde..07e64cffa2 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 @@ -296,6 +296,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 11f4707f8a..b10fdfe4c9 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 @@ -320,6 +320,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 11746559a5..42144a996a 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 @@ -214,6 +214,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -387,6 +395,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_child.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 a49bd678d9..4c7d4c8583 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -2046,6 +2046,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_5:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 20, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 e38df9c0f5..bd846c93b2 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 @@ -323,6 +323,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 49f1117d41..8481055813 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 @@ -335,6 +335,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -359,6 +367,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_2_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 d9588faa15..77663604d5 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 @@ -314,6 +314,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 3e3aca24f2..6f4a098ef7 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 @@ -163,6 +163,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 9f7f7d5b86..be1b64b05b 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 @@ -493,6 +493,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -514,6 +522,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 5f7217472e..35f8b341dd 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 @@ -234,6 +234,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 abfaa48ffd..13d3e90a3c 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,6 +57,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 8b526a9984..2a96961755 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 @@ -64,6 +64,14 @@ test.register_message_test( meter_value = 27}) )} }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } } ) 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 56ffdd939d..1905fec5f6 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,6 +68,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 6393613025..69034f9b5a 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua @@ -146,6 +146,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7cbc6e38e5..cac8b883e4 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 @@ -244,6 +244,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7ff628f974..3671cd4029 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua @@ -95,8 +95,16 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } } } + } ) test.register_message_test( 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 00056c753a..2c9b04d82d 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 @@ -471,6 +471,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 b332713319..ea14555a0a 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua @@ -256,6 +256,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 9c800e7070..9c879806f6 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 @@ -303,7 +303,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_fibaro_roller_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( 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 ce5ed9fb81..5431ed1e70 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 @@ -233,7 +233,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_qubino_flush_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index bdc41956ec..f1a841c626 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -214,7 +214,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -251,7 +251,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -374,7 +374,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -415,7 +415,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua index 4335ae3b41..0b2d209f2a 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_springs_window_fashion_shade.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index bf60c1797b..bc3a087093 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -304,7 +304,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_window_shade_switch_multilevel.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( From 0062c4839b995a6e52abbdb24ff8157b5b89ad7f Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Mon, 18 Aug 2025 10:20:44 -0500 Subject: [PATCH 074/449] sonos: Fix websocket reconnect task There is a race condition when a sonos connection calls close on itself. It immediately calls on_close which will spawn a task to reconnect. The reconnect task checks if the connection is running which depends on the socket being closed and cleaned up. Instead, call on_close after the socket has been closed and cleaned up. --- .../sonos/src/api/sonos_websocket_router.lua | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua index 73e04efb64..7e15855e89 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua @@ -44,6 +44,16 @@ cosock.spawn(function() if wss ~= nil then log.trace(string.format("Closing websocket for player %s", unique_key)) wss:close(CloseCode.normal(), "Shutdown requested by client") + local ws_id = wss.id + for _, uuid in ipairs((listener_ids_for_socket[ws_id] or {})) do + local listener = listeners[uuid] + + if listener ~= nil then + listener.on_close(uuid) + end + listeners[uuid] = nil + end + listener_ids_for_socket[ws_id] = nil end websockets[unique_key] = nil end @@ -292,17 +302,7 @@ function SonosWebSocketRouter.close_socket_for_player(target) local ws = websockets[target] if ws ~= nil then - local ws_id = ws.id table.insert(pending_close, target) - for _, uuid in ipairs((listener_ids_for_socket[ws_id] or {})) do - local listener = listeners[uuid] - - if listener ~= nil then - listener.on_close(uuid) - end - listeners[uuid] = nil - end - listener_ids_for_socket[ws_id] = nil return true else return nil, string.format("No currently open connection for %s", target) From 41627a898dc18db9cca280107db00cbc37ad0ba3 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 21 Jul 2025 19:26:41 -0500 Subject: [PATCH 075/449] add support for matter hrap device types --- drivers/SmartThings/matter-hrap/config.yml | 6 + .../SmartThings/matter-hrap/fingerprints.yml | 11 + .../network-infrastructure-manager.yml | 16 + .../profiles/thread-border-router.yml | 14 + .../client/commands/DatasetResponse.lua | 103 ++++++ .../client/commands/init.lua | 23 ++ .../src/ThreadBorderRouterManagement/init.lua | 148 ++++++++ .../attributes/ActiveDatasetTimestamp.lua | 68 ++++ .../server/attributes/BorderRouterName.lua | 69 ++++ .../server/attributes/InterfaceEnabled.lua | 69 ++++ .../server/attributes/ThreadVersion.lua | 68 ++++ .../server/attributes/init.lua | 24 ++ .../commands/GetActiveDatasetRequest.lua | 79 ++++ .../server/commands/init.lua | 28 ++ .../types/Feature.lua | 54 +++ .../types/init.lua | 15 + .../src/WiFiNetworkManagement/init.lua | 58 +++ .../server/attributes/Ssid.lua | 68 ++++ .../server/attributes/init.lua | 24 ++ .../src/embedded-cluster-utils.lua | 62 ++++ drivers/SmartThings/matter-hrap/src/init.lua | 217 +++++++++++ .../test_thread_border_router_network.lua | 342 ++++++++++++++++++ 22 files changed, 1566 insertions(+) create mode 100644 drivers/SmartThings/matter-hrap/config.yml create mode 100644 drivers/SmartThings/matter-hrap/fingerprints.yml create mode 100644 drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml create mode 100644 drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua create mode 100644 drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua create mode 100644 drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua create mode 100644 drivers/SmartThings/matter-hrap/src/init.lua create mode 100644 drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua diff --git a/drivers/SmartThings/matter-hrap/config.yml b/drivers/SmartThings/matter-hrap/config.yml new file mode 100644 index 0000000000..21d79bc8ec --- /dev/null +++ b/drivers/SmartThings/matter-hrap/config.yml @@ -0,0 +1,6 @@ +name: 'Matter HRAP' +packageKey: 'matter-hrap' +permissions: + matter: {} +description: "SmartThings driver for Matter HRAP devices" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/matter-hrap/fingerprints.yml b/drivers/SmartThings/matter-hrap/fingerprints.yml new file mode 100644 index 0000000000..7b768709c2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/fingerprints.yml @@ -0,0 +1,11 @@ +matterGeneric: + - id: "matter/network-manager" + deviceLabel: Matter Network Infrastructure Manager + deviceTypes: + - id: 0x0090 # NIM + deviceProfileName: network-infrastructure-manager + - id: "matter/thread-border-router" + deviceLabel: Matter Thread Border Router + deviceTypes: + - id: 0x0091 # TBR + deviceProfileName: thread-border-router diff --git a/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml new file mode 100644 index 0000000000..725676c502 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/network-infrastructure-manager.yml @@ -0,0 +1,16 @@ +name: network-infrastructure-manager +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: wifiInformation + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml new file mode 100644 index 0000000000..b879dd2e76 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/profiles/thread-border-router.yml @@ -0,0 +1,14 @@ +name: thread-border-router +components: +- id: main + capabilities: + - id: threadBorderRouter + version: 1 + - id: threadNetwork + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Networking diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua new file mode 100644 index 0000000000..0cf8facc53 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/DatasetResponse.lua @@ -0,0 +1,103 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DatasetResponse = {} + +DatasetResponse.NAME = "DatasetResponse" +DatasetResponse.ID = 0x0002 +DatasetResponse.field_defs = { + { + name = "dataset", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.OctetString1", + }, +} + +function DatasetResponse:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function DatasetResponse:build_test_command_response(device, endpoint_id, dataset, interaction_status) + local function init(self, device, endpoint_id, dataset) + local out = {} + local args = {dataset} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = DatasetResponse, + __tostring = DatasetResponse.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID, + false, + true + ) + end + local self_request = init(self, device, endpoint_id, dataset) + return self._cluster:build_test_command_response( + device, + endpoint_id, + self._cluster.ID, + self.ID, + self_request.info_blocks[1].tlv, + interaction_status + ) +end + +function DatasetResponse:init() + return nil +end + +function DatasetResponse:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DatasetResponse:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(DatasetResponse, {__call = DatasetResponse.init}) + +return DatasetResponse diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua new file mode 100644 index 0000000000..3c8bee49fa --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/client/commands/init.lua @@ -0,0 +1,23 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.client.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementClientCommands = {} + +function ThreadBorderRouterManagementClientCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementClientCommands, command_mt) + +return ThreadBorderRouterManagementClientCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua new file mode 100644 index 0000000000..3b3485d03d --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/init.lua @@ -0,0 +1,148 @@ +local cluster_base = require "st.matter.cluster_base" +local ThreadBorderRouterManagementServerAttributes = require "ThreadBorderRouterManagement.server.attributes" +local ThreadBorderRouterManagementServerCommands = require "ThreadBorderRouterManagement.server.commands" +local ThreadBorderRouterManagementClientCommands = require "ThreadBorderRouterManagement.client.commands" +local ThreadBorderRouterManagementTypes = require "ThreadBorderRouterManagement.types" + +local ThreadBorderRouterManagement = {} + +ThreadBorderRouterManagement.ID = 0x0452 +ThreadBorderRouterManagement.NAME = "ThreadBorderRouterManagement" +ThreadBorderRouterManagement.server = {} +ThreadBorderRouterManagement.client = {} +ThreadBorderRouterManagement.server.attributes = ThreadBorderRouterManagementServerAttributes:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.server.commands = ThreadBorderRouterManagementServerCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.client.commands = ThreadBorderRouterManagementClientCommands:set_parent_cluster(ThreadBorderRouterManagement) +ThreadBorderRouterManagement.types = ThreadBorderRouterManagementTypes + +function ThreadBorderRouterManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "BorderRouterName", + [0x0001] = "BorderAgentID", + [0x0002] = "ThreadVersion", + [0x0003] = "InterfaceEnabled", + [0x0004] = "ActiveDatasetTimestamp", + [0x0005] = "PendingDatasetTimestamp", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function ThreadBorderRouterManagement:get_server_command_by_id(command_id) + local server_id_map = { + [0x0000] = "GetActiveDatasetRequest", + [0x0001] = "GetPendingDatasetRequest", + [0x0003] = "SetActiveDatasetRequest", + [0x0004] = "SetPendingDatasetRequest", + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +function ThreadBorderRouterManagement:get_client_command_by_id(command_id) + local client_id_map = { + [0x0002] = "DatasetResponse", + } + if client_id_map[command_id] ~= nil then + return self.client.commands[client_id_map[command_id]] + end + return nil +end + +ThreadBorderRouterManagement.attribute_direction_map = { + ["BorderRouterName"] = "server", + ["BorderAgentID"] = "server", + ["ThreadVersion"] = "server", + ["InterfaceEnabled"] = "server", + ["ActiveDatasetTimestamp"] = "server", + ["PendingDatasetTimestamp"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.attribute_direction_map[alias] = "server" + end + end +end + +ThreadBorderRouterManagement.command_direction_map = { + ["GetActiveDatasetRequest"] = "server", + ["GetPendingDatasetRequest"] = "server", + ["SetActiveDatasetRequest"] = "server", + ["SetPendingDatasetRequest"] = "server", + ["DatasetResponse"] = "client", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "server" + end + end +end + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.client.commands") + if has_aliases then + for alias, _ in pairs(aliases) do + ThreadBorderRouterManagement.command_direction_map[alias] = "client" + end + end +end + +ThreadBorderRouterManagement.FeatureMap = ThreadBorderRouterManagement.types.Feature + +function ThreadBorderRouterManagement.are_features_supported(feature, feature_map) + if (ThreadBorderRouterManagement.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].attributes[key] +end +ThreadBorderRouterManagement.attributes = {} +setmetatable(ThreadBorderRouterManagement.attributes, attribute_helper_mt) + +local command_helper_mt = {} +command_helper_mt.__index = function(self, key) + local direction = ThreadBorderRouterManagement.command_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown command %s on cluster %s", key, ThreadBorderRouterManagement.NAME)) + end + return ThreadBorderRouterManagement[direction].commands[key] +end +ThreadBorderRouterManagement.commands = {} +setmetatable(ThreadBorderRouterManagement.commands, command_helper_mt) + +local event_helper_mt = {} +event_helper_mt.__index = function(self, key) + return ThreadBorderRouterManagement.server.events[key] +end +ThreadBorderRouterManagement.events = {} +setmetatable(ThreadBorderRouterManagement.events, event_helper_mt) + +setmetatable(ThreadBorderRouterManagement, {__index = cluster_base}) + +return ThreadBorderRouterManagement + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua new file mode 100644 index 0000000000..b0dc67b590 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ActiveDatasetTimestamp.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ActiveDatasetTimestamp = { + ID = 0x0004, + NAME = "ActiveDatasetTimestamp", + base_type = require "st.matter.data_types.Uint64", +} + +function ActiveDatasetTimestamp:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ActiveDatasetTimestamp:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ActiveDatasetTimestamp:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ActiveDatasetTimestamp: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 ActiveDatasetTimestamp:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ActiveDatasetTimestamp, {__call = ActiveDatasetTimestamp.new_value, __index = ActiveDatasetTimestamp.base_type}) +return ActiveDatasetTimestamp + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua new file mode 100644 index 0000000000..33fd11c111 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/BorderRouterName.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local BorderRouterName = { + ID = 0x0000, + NAME = "BorderRouterName", + base_type = require "st.matter.data_types.UTF8String1", +} + +function BorderRouterName:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function BorderRouterName:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function BorderRouterName:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function BorderRouterName:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function BorderRouterName: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 BorderRouterName:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(BorderRouterName, {__call = BorderRouterName.new_value, __index = BorderRouterName.base_type}) +return BorderRouterName + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua new file mode 100644 index 0000000000..c2676bfaf0 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/InterfaceEnabled.lua @@ -0,0 +1,69 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local InterfaceEnabled = { + ID = 0x0003, + NAME = "InterfaceEnabled", + base_type = require "st.matter.data_types.Boolean", +} + +function InterfaceEnabled:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function InterfaceEnabled:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function InterfaceEnabled:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function InterfaceEnabled:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function InterfaceEnabled: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 InterfaceEnabled:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(InterfaceEnabled, {__call = InterfaceEnabled.new_value, __index = InterfaceEnabled.base_type}) +return InterfaceEnabled + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua new file mode 100644 index 0000000000..6630c863e9 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/ThreadVersion.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local ThreadVersion = { + ID = 0x0002, + NAME = "ThreadVersion", + base_type = require "st.matter.data_types.Uint16", +} + +function ThreadVersion:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function ThreadVersion:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function ThreadVersion:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function ThreadVersion: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 ThreadVersion:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(ThreadVersion, {__call = ThreadVersion.new_value, __index = ThreadVersion.base_type}) +return ThreadVersion + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua new file mode 100644 index 0000000000..96cad47fdd --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +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("ThreadBorderRouterManagement.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 ThreadBorderRouterManagementServerAttributes = {} + +function ThreadBorderRouterManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerAttributes, attr_mt) + +return ThreadBorderRouterManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua new file mode 100644 index 0000000000..b63982575e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/GetActiveDatasetRequest.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GetActiveDatasetRequest = {} + +GetActiveDatasetRequest.NAME = "GetActiveDatasetRequest" +GetActiveDatasetRequest.ID = 0x0000 +GetActiveDatasetRequest.field_defs = { +} + +function GetActiveDatasetRequest:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GetActiveDatasetRequest, + __tostring = GetActiveDatasetRequest.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function GetActiveDatasetRequest:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GetActiveDatasetRequest:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function GetActiveDatasetRequest:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(GetActiveDatasetRequest, {__call = GetActiveDatasetRequest.init}) + +return GetActiveDatasetRequest diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua new file mode 100644 index 0000000000..ec75068d8e --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/server/commands/init.lua @@ -0,0 +1,28 @@ +local command_mt = {} +command_mt.__command_cache = {} +command_mt.__index = function(self, key) + if command_mt.__command_cache[key] == nil then + local req_loc = string.format("ThreadBorderRouterManagement.server.commands.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) + end + return command_mt.__command_cache[key] +end + +local ThreadBorderRouterManagementServerCommands = {} + +function ThreadBorderRouterManagementServerCommands:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(ThreadBorderRouterManagementServerCommands, command_mt) + +local status, aliases = pcall(require, "st.matter.clusters.aliases.ThreadBorderRouterManagement.server.commands") +if status then + aliases:add_to_class(ThreadBorderRouterManagementServerCommands) +end + +return ThreadBorderRouterManagementServerCommands + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua new file mode 100644 index 0000000000..c8116cd481 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/Feature.lua @@ -0,0 +1,54 @@ +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.PAN_CHANGE = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + PAN_CHANGE = 0x0001, +} + +Feature.is_pan_change_set = function(self) + return (self.value & self.PAN_CHANGE) ~= 0 +end + +Feature.set_pan_change = function(self) + if self.value ~= nil then + self.value = self.value | self.PAN_CHANGE + else + self.value = self.PAN_CHANGE + end +end + +Feature.unset_pan_change = function(self) + self.value = self.value & (~self.PAN_CHANGE & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.PAN_CHANGE + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_pan_change_set = Feature.is_pan_change_set, + set_pan_change = Feature.set_pan_change, + unset_pan_change = Feature.unset_pan_change, +} + +Feature.augment_type = function(cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua new file mode 100644 index 0000000000..3aece4eef2 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/ThreadBorderRouterManagement/types/init.lua @@ -0,0 +1,15 @@ +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("ThreadBorderRouterManagement.types." .. key) + end + return types_mt.__types_cache[key] +end + +local ThreadBorderRouterManagementTypes = {} + +setmetatable(ThreadBorderRouterManagementTypes, types_mt) + +return ThreadBorderRouterManagementTypes + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua new file mode 100644 index 0000000000..061ee5854f --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/init.lua @@ -0,0 +1,58 @@ +local cluster_base = require "st.matter.cluster_base" +local WiFiNetworkManagementServerAttributes = require "WiFiNetworkManagement.server.attributes" + +local WiFiNetworkManagement = {} + +WiFiNetworkManagement.ID = 0x0451 +WiFiNetworkManagement.NAME = "WiFiNetworkManagement" +WiFiNetworkManagement.server = {} +WiFiNetworkManagement.client = {} +WiFiNetworkManagement.server.attributes = WiFiNetworkManagementServerAttributes:set_parent_cluster(WiFiNetworkManagement) + +function WiFiNetworkManagement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "Ssid", + [0x0001] = "PassphraseSurrogate", + [0xFFF9] = "AcceptedCommandList", + [0xFFFA] = "EventList", + [0xFFFB] = "AttributeList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +WiFiNetworkManagement.attribute_direction_map = { + ["Ssid"] = "server", + ["PassphraseSurrogate"] = "server", + ["AcceptedCommandList"] = "server", + ["EventList"] = "server", + ["AttributeList"] = "server", +} + +do + local has_aliases, aliases = pcall(require, "st.matter.clusters.aliases.WiFiNetworkManagement.server.attributes") + if has_aliases then + for alias, _ in pairs(aliases) do + WiFiNetworkManagement.attribute_direction_map[alias] = "server" + end + end +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = WiFiNetworkManagement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, WiFiNetworkManagement.NAME)) + end + return WiFiNetworkManagement[direction].attributes[key] +end +WiFiNetworkManagement.attributes = {} +setmetatable(WiFiNetworkManagement.attributes, attribute_helper_mt) + +setmetatable(WiFiNetworkManagement, {__index = cluster_base}) + +return WiFiNetworkManagement + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua new file mode 100644 index 0000000000..e7a2a43fb5 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/Ssid.lua @@ -0,0 +1,68 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local Ssid = { + ID = 0x0000, + NAME = "Ssid", + base_type = require "st.matter.data_types.OctetString1", +} + +function Ssid:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function Ssid:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function Ssid:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function Ssid: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 Ssid:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(Ssid, {__call = Ssid.new_value, __index = Ssid.base_type}) +return Ssid + diff --git a/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua new file mode 100644 index 0000000000..de0b55c8b1 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/WiFiNetworkManagement/server/attributes/init.lua @@ -0,0 +1,24 @@ +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("WiFiNetworkManagement.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 WiFiNetworkManagementServerAttributes = {} + +function WiFiNetworkManagementServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(WiFiNetworkManagementServerAttributes, attr_mt) + +return WiFiNetworkManagementServerAttributes + diff --git a/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua new file mode 100644 index 0000000000..ca36cd7562 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/embedded-cluster-utils.lua @@ -0,0 +1,62 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_api_13 = { + [clusters.ThreadBorderRouterManagement.ID] = clusters.ThreadBorderRouterManagement, +} + +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 < 13 and embedded_clusters_api_13[cluster_id] ~= nil then + local embedded_cluster = embedded_clusters_api_13[cluster_id] + if not opts then opts = {} end + 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-hrap/src/init.lua b/drivers/SmartThings/matter-hrap/src/init.lua new file mode 100644 index 0000000000..df05e7ce83 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/init.lua @@ -0,0 +1,217 @@ +-- Copyright 2025 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 MatterDriver = require "st.matter.driver" +local data_types = require "st.matter.data_types" +local im = require "st.matter.interaction_model" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local lustre_utils = require "lustre.utils" +local st_utils = require "st.utils" +local version = require "version" +local log = require "log" + +local CURRENT_ACTIVE_DATASET_TIMESTAMP = "__CURRENT_ACTIVE_DATASET_TIMESTAMP" +local GET_ACTIVE_DATASET_RETRY_ATTEMPTS = "__GET_ACTIVE_DATASET_RETRY_ATTEMPTS" + +-- Include driver-side definitions when lua libs api version is <13 +if version.api < 13 then + clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" + clusters.WiFiNetworkManagement = require "WiFiNetworkManagement" +end + + +--[[ ATTRIBUTE HANDLERS ]]-- + +local function border_router_name_attribute_handler(driver, device, ib) + -- per the spec, the recommended attribute format is ._meshcop._udp. This logic removes the MeshCoP suffix IFF it is present + local meshCop_name = ib.data.value + local terminal_display_char = (string.find(meshCop_name, "._meshcop._udp") or 64) - 1 -- where 64-1=63, the maximum allowed length for BorderRouterName + local display_name = string.sub(meshCop_name, 1, terminal_display_char) + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.borderRouterName({ value = display_name })) +end + +local function ssid_attribute_handler(driver, device, ib) + if (ib.data.value == string.char(data_types.Null.ID) or ib.data.value == nil) then -- Matter TLV-encoded NULL or Lua-encoded NULL + device.log.info("No primary Wi-Fi network is available") + return + end + local valid_utf8, utf8_err = lustre_utils.validate_utf8(ib.data.value) + if valid_utf8 then + device:emit_event_for_endpoint(ib.endpoint, capabilities.wifiInformation.ssid({ value = ib.data.value })) + else + device.log.info("UTF-8 validation of SSID failed: Error: '"..utf8_err.."'") + end +end + +local function thread_interface_enabled_attribute_handler(driver, device, ib) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("enabled")) + else + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadInterfaceState("disabled")) + end +end + +-- Spec uses TLV encoding of Thread Version, which should be mapped to a more human-readable name +local VERSION_TLV_MAP = { + [1] = "1.0.0", + [2] = "1.1.0", + [3] = "1.2.0", + [4] = "1.3.0", + [5] = "1.4.0", +} + +local function thread_version_attribute_handler(driver, device, ib) + local version_name = VERSION_TLV_MAP[ib.data.value] + if version_name then + device:emit_event_for_endpoint(ib.endpoint, capabilities.threadBorderRouter.threadVersion({ value = version_name })) + else + device.log.warn("The received TLV-encoded Thread version does not have a provided mapping to a human-readable version format") + end +end + +local function active_dataset_timestamp_handler(driver, device, ib) + if not ib.data.value then + device.log.info("No Thread operational dataset configured") + elseif ib.data.value ~= device:get_field(CURRENT_ACTIVE_DATASET_TIMESTAMP) then + device:send(clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(device, ib.endpoint_id)) + device:set_field(CURRENT_ACTIVE_DATASET_TIMESTAMP, ib.data.value, { persist = true }) + end +end + + +--[[ COMMAND HANDLERS ]]-- + +local threadNetwork = capabilities.threadNetwork +local TLV_TYPE_ATTR_MAP = { + [0] = threadNetwork.channel, + [1] = threadNetwork.panId, + [2] = threadNetwork.extendedPanId, + [3] = threadNetwork.networkName, + -- [4] intentionally omitted, refers to the MeshCoP PSKc + [5] = threadNetwork.networkKey, +} + +local function dataset_response_handler(driver, device, ib) + if not ib.info_block.data.elements.dataset then + device.log.debug_with({ hub_logs = true }, "Received empty Thread operational dataset") + return + end + + local operational_dataset_length = ib.info_block.data.elements.dataset.byte_length + local spec_defined_max_dataset_length = 254 + if operational_dataset_length > spec_defined_max_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset is too long") + return + end + + -- parse dataset + local operational_dataset = ib.info_block.data.elements.dataset.value + local cur_byte = 1 + while cur_byte + 1 <= operational_dataset_length do + local tlv_type = string.byte(operational_dataset, cur_byte) + local tlv_length = string.byte(operational_dataset, cur_byte + 1) + if (cur_byte + 1 + tlv_length) > operational_dataset_length then + device.log.error_with({ hub_logs = true }, "Received Thread operational dataset has a malformed TLV encoding") + return + end + local tlv_mapped_attr = TLV_TYPE_ATTR_MAP[tlv_type] + if tlv_mapped_attr then + -- extract the value from a TLV-encoded message. Message format: byte tag + byte length + length byte value + local tlv_value = operational_dataset:sub(cur_byte + 2, cur_byte + 1 + tlv_length) + -- format data as required by threadNetwork attribute properties + if tlv_mapped_attr == threadNetwork.channel or tlv_mapped_attr == threadNetwork.panId then + tlv_value = st_utils.deserialize_int(tlv_value, tlv_length) + elseif tlv_mapped_attr ~= threadNetwork.networkName then + tlv_value = st_utils.bytes_to_hex_string(tlv_value) + end + device:emit_event(tlv_mapped_attr({ value = tlv_value })) + end + cur_byte = cur_byte + 2 + tlv_length + end +end + +local function get_active_dataset_response_handler(driver, device, ib) + if ib.status == im.InteractionResponse.Status.FAILURE then + -- per spec, on a GetActiveDatasetRequest failure, a failure response is sent over the same command. + -- on failure, retry the read up to 3 times before failing out. + local retries_attempted = device:get_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS) or 0 + if retries_attempted < 3 then + device.log.error_with({ hub_logs = true }, "Failed to retrieve Thread operational dataset. Retrying " .. retries_attempted + 1 .. "/3") + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, retries_attempted + 1) + else + -- do not retry again, but reset the count to 0. + device:set_field(GET_ACTIVE_DATASET_RETRY_ATTEMPTS, 0) + end + elseif ib.status == im.InteractionResponse.Status.UNSUPPORTED_ACCESS then + device.log.error_with({ hub_logs = true }, + "Failed to retrieve Thread operational dataset, since the GetActiveDatasetRequest command was not executed over CASE" + ) + end +end + + +--[[ LIFECYCLE HANDLERS ]]-- + +local function device_init(driver, device) + device:subscribe() +end + + +--[[ MATTER DRIVER TEMPLATE ]]-- + +local matter_driver_template = { + lifecycle_handlers = { + init = device_init, + }, + matter_handlers = { + attr = { + [clusters.WiFiNetworkManagement.ID] = { + [clusters.WiFiNetworkManagement.attributes.Ssid.ID] = ssid_attribute_handler, + }, + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.attributes.BorderRouterName.ID] = border_router_name_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ThreadVersion.ID] = thread_version_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled.ID] = thread_interface_enabled_attribute_handler, + [clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp.ID] = active_dataset_timestamp_handler, + } + }, + cmd_response = { + [clusters.ThreadBorderRouterManagement.ID] = { + [clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse.ID] = dataset_response_handler, + [clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest.ID] = get_active_dataset_response_handler, + } + } + }, + subscribed_attributes = { + [capabilities.threadBorderRouter.ID] = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + }, + [capabilities.wifiInformation.ID] = { + clusters.WiFiNetworkManagement.attributes.Ssid, + } + }, + supported_capabilities = { + capabilities.threadBorderRouter, + capabilities.threadNetwork, + capabilities.wifiInformation, + } +} + +local matter_driver = MatterDriver("matter-hrap", matter_driver_template) +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() 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 new file mode 100644 index 0000000000..0467e19de3 --- /dev/null +++ b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua @@ -0,0 +1,342 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local data_types = require "st.matter.data_types" +local capabilities = require "st.capabilities" + +local clusters = require "st.matter.clusters" +clusters.ThreadBorderRouterManagement = require "ThreadBorderRouterManagement" +clusters.WifiNetworkMangement = require "WiFiNetworkManagement" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("network-infrastructure-manager.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + 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.ThreadBorderRouterManagement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.WifiNetworkMangement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0090, device_type_revision = 1,} -- Network Infrastructure Manager + } + } + } +}) + +local cluster_subscribe_list = { + clusters.ThreadBorderRouterManagement.attributes.ActiveDatasetTimestamp, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion, + clusters.WifiNetworkMangement.attributes.Ssid, +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + 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( + "ThreadVersion should display the correct stringified version", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 3 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.2.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 4 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.3.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 5 + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadVersion({ value = "1.4.0" })) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.ThreadVersion:build_test_report_data( + mock_device, 1, 6 + ) + }) + end +) + +test.register_message_test( + "InterfaceEnabled should correctly display enabled or disabled", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("enabled")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.InterfaceEnabled:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.threadInterfaceState("disabled")) + } + } +) + +test.register_message_test( + "BorderRouterName should correctly display the given name", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "jane bar._meshcop._udp") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "jane bar"})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ThreadBorderRouterManagement.attributes.BorderRouterName:build_test_report_data(mock_device, 1, "john foo no suffix") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.threadBorderRouter.borderRouterName({ value = "john foo no suffix"})) + }, + } +) + +test.register_message_test( + "wifiInformation capability should correctly display the Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, "test name for ssid!") + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.wifiInformation.ssid({ value = "test name for ssid!" })) + } + } +) + +test.register_message_test( + "Null-valued ssid (TLV 0x14) should correctly fail", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(data_types.Null.ID)) + } + } + } +) + +test.register_message_test( + "Ssid inputs using non-UTF8 encoding should not display an Ssid", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.WifiNetworkMangement.attributes.Ssid:build_test_report_data(mock_device, 1, string.char(0xC0)) -- 0xC0 never appears in utf8 + } + } + } +) + +local hex_dataset = [[ +0E 08 00 00 68 87 D0 B2 00 00 00 03 00 00 18 35 +06 00 04 00 1F FF C0 02 08 25 31 25 A9 B2 16 7F +35 07 08 FD 6E D1 57 02 B4 CD BF 05 10 33 AF 36 +F8 13 8E 8F F9 50 6D 67 22 9B FD F2 40 03 0D 53 +54 2D 35 30 33 32 30 30 31 31 39 36 01 02 D9 78 +04 10 E2 29 D8 2A 84 B2 7D A1 AC 8D D8 71 64 AC +66 7F 0C 04 02 A0 FF F8 +]] + +local serializable_hex_dataset = hex_dataset:gsub("%s+", ""):gsub("..", function(cc) + return string.char(tonumber(cc, 16)) +end) + +test.register_coroutine_test( + "Thread DatasetResponse parsing should emit the correct capability events on an ActiveDatasetTimestamp update. Else, nothing should happen", + function() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + test.wait_for_events() + + -- after some amount of time, a device init occurs or we re-subscribe for other reasons. + -- Since no change to the ActiveDatasetTimestamp has occurred, no re-read should occur + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 1 + ) + }) + test.wait_for_events() + + -- after some more amount of time, a device init occurs or we re-subscribe for other reasons. + -- This time, their ActiveDatasetTimestamp has updated, so we should re-read the operational dataset. + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.attributes.ActiveDatasetTimestamp:build_test_report_data( + mock_device, + 1, + 2 + ) + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ThreadBorderRouterManagement.server.commands.GetActiveDatasetRequest(mock_device, 1), + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ThreadBorderRouterManagement.client.commands.DatasetResponse:build_test_command_response( + mock_device, + 1, + serializable_hex_dataset + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.channel({ value = 24 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.extendedPanId({ value = "253125a9b2167f35" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkKey({ value = "33af36f8138e8ff9506d67229bfdf240" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.networkName({ value = "ST-5032001196" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threadNetwork.panId({ value = 55672 })) + ) + end +) + +test.run_registered_tests() From 981db5ffc48449de9662a2c6cd579217767ad9d9 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Fri, 18 Apr 2025 13:24:24 -0500 Subject: [PATCH 076/449] Fix unit tests for new features in 58 Lua libs release New features include: * Init messages being generated by the hub instead of in the lua libs * Modular profiles * More native handler registration by default --- .../src/test/test_cook_top.lua | 19 +- .../src/test/test_dishwasher.lua | 8 +- .../src/test/test_extractor_hood.lua | 16 +- .../src/test/test_laundry_dryer.lua | 5 +- .../src/test/test_laundry_washer.lua | 6 +- .../src/test/test_matter_appliance_rpc_5.lua | 108 ++-- .../src/test/test_microwave_oven.lua | 8 +- .../matter-appliance/src/test/test_oven.lua | 8 +- .../src/test/test_refrigerator.lua | 6 +- .../test/test_matter_button_parent_child.lua | 1 + .../src/test/test_matter_multi_button.lua | 26 +- .../src/test/test_battery_storage.lua | 9 +- .../matter-energy/src/test/test_evse.lua | 11 +- .../src/test/test_evse_energy_meas.lua | 11 +- .../src/test/test_solar_power.lua | 10 +- .../src/test/test_aqara_matter_lock.lua | 17 +- .../src/test/test_bridged_matter_lock.lua | 90 +-- .../matter-lock/src/test/test_matter_lock.lua | 11 +- .../src/test/test_matter_lock_battery.lua | 24 +- .../test/test_matter_lock_batteryLevel.lua | 39 +- .../src/test/test_matter_lock_codes.lua | 28 +- .../src/test/test_matter_lock_cota.lua | 21 +- .../src/test/test_matter_lock_unlatch.lua | 26 +- .../src/test/test_new_matter_lock.lua | 26 +- .../src/test/test_new_matter_lock_battery.lua | 200 ++----- .../src/test/test_matter_media_speaker.lua | 555 +++++++++--------- .../test/test_matter_media_video_player.lua | 135 +++-- drivers/SmartThings/matter-rvc/src/init.lua | 9 +- .../matter-rvc/src/test/test_matter_rvc.lua | 11 +- .../test/test_matter_air_quality_sensor.lua | 3 + .../src/test/test_matter_flow_sensor.lua | 3 +- .../test/test_matter_freeze_leak_sensor.lua | 95 ++- .../src/test/test_matter_pressure_sensor.lua | 12 +- .../src/test/test_matter_rain_sensor.lua | 18 +- .../src/test/test_matter_sensor.lua | 5 +- .../src/test/test_matter_sensor_battery.lua | 9 +- .../test/test_matter_sensor_featuremap.lua | 20 +- .../src/test/test_matter_sensor_rpc.lua | 2 - .../src/test/test_matter_smoke_co_alarm.lua | 7 +- .../test_matter_smoke_co_alarm_battery.lua | 4 +- .../test/test_aqara_climate_sensor_w100.lua | 13 +- .../src/test/test_aqara_light_switch_h2.lua | 22 +- .../src/test/test_electrical_sensor.lua | 12 +- .../src/test/test_matter_button.lua | 11 +- .../src/test/test_matter_light_fan.lua | 9 +- .../src/test/test_matter_multi_button.lua | 29 +- .../test_matter_multi_button_switch_mcd.lua | 119 ++-- .../test/test_matter_switch_device_types.lua | 74 ++- .../test_multi_switch_parent_child_lights.lua | 18 +- .../test_multi_switch_parent_child_plugs.lua | 17 +- .../src/test/test_third_reality_mk1.lua | 12 +- .../src/test/test_matter_air_purifier.lua | 1 + .../test/test_matter_air_purifier_modular.lua | 61 +- .../src/test/test_matter_heat_pump.lua | 26 +- .../src/test/test_matter_room_ac.lua | 40 +- .../src/test/test_matter_room_ac_modular.lua | 90 +-- .../src/test/test_matter_thermo_battery.lua | 2 + .../test/test_matter_thermo_featuremap.lua | 3 +- ...st_matter_thermo_multiple_device_types.lua | 119 ++-- .../src/test/test_matter_thermostat.lua | 33 +- ...est_matter_thermostat_composed_bridged.lua | 37 +- .../test/test_matter_thermostat_modular.lua | 40 +- .../src/test/test_matter_thermostat_rpc5.lua | 145 +++++ .../src/test/test_matter_water_heater.lua | 52 +- .../src/test/test_matter_window_covering.lua | 92 ++- .../test/test_frient_contact_sensor_pro.lua | 8 + .../src/test/test_zigbee_contact.lua | 8 + .../src/test/test_zigbee_contact_tyco.lua | 9 + .../zigbee-humidity-sensor/src/init.lua | 2 +- .../src/test/test_aqara_sensor.lua | 9 + .../src/test/test_humidity_plaid_systems.lua | 9 + .../src/test/test_humidity_temperature.lua | 9 + .../test_humidity_temperature_battery.lua | 9 + .../test/test_humidity_temperature_sensor.lua | 8 + .../test_all_capabilities_zigbee_motion.lua | 9 + .../test/test_frient_motion_sensor_pro.lua | 8 + .../src/test/test_zigbee_power_meter.lua | 4 +- ..._zigbee_power_meter_consumption_report.lua | 2 +- .../src/test/test_frient_smoke_detector.lua | 8 + .../src/test/test_zigbee_sound_sensor.lua | 8 + .../test/test_all_capability_zigbee_bulb.lua | 13 +- .../src/test/test_frient_switch.lua | 8 + .../src/test/test_multi_switch_power.lua | 16 + .../test/test_robb_smarrt_2-wire_dimmer.lua | 8 + .../src/test/test_robb_smarrt_knob_dimmer.lua | 8 + .../src/test/test_switch_power.lua | 4 +- .../src/test/test_zigbee_ezex_switch.lua | 8 + .../src/test/test_zigbee_thermostat.lua | 2 +- .../test_centralite_water_leak_sensor.lua | 8 + .../test/test_frient_water_leak_sensor.lua | 8 + .../test/test_samjin_water_leak_sensor.lua | 8 + .../src/test/test_sinope_zigbee_water.lua | 8 + .../test_smartthings_water_leak_sensor.lua | 8 + .../src/test/test_zigbee_water.lua | 8 + .../src/fibaro-flood-sensor/init.lua | 12 - .../test/test_fibaro_door_window_sensor_1.lua | 16 + ...ro_door_window_sensor_with_temperature.lua | 16 + .../src/test/test_fibaro_flood_sensor.lua | 8 + .../src/test/test_fibaro_motion_sensor.lua | 16 + .../src/test/test_generic_sensor.lua | 36 +- .../test_smartthings_water_leak_sensor.lua | 16 + .../src/test/test_aeotec_dimmer_switch.lua | 8 + .../src/test/test_aeotec_nano_dimmer.lua | 8 + .../test/test_fibaro_walli_double_switch.lua | 16 + .../src/test/test_multichannel_device.lua | 8 + .../src/test/test_qubino_din_dimmer.lua | 8 + .../src/test/test_qubino_flush_2_relay.lua | 16 + .../src/test/test_qubino_flush_dimmer.lua | 8 + ...t_qubino_temperature_sensor_with_power.lua | 8 + .../src/test/test_zooz_double_plug.lua | 16 + .../test/test_zwave_dimmer_power_energy.lua | 8 + .../test/test_zwave_switch_electric_meter.lua | 8 + .../test/test_zwave_switch_energy_meter.lua | 8 + .../test/test_zwave_switch_power_meter.lua | 8 + .../test/test_aeotec_radiator_thermostat.lua | 8 + .../src/test/test_fibaro_heat_controller.lua | 8 + .../test/test_popp_radiator_thermostat.lua | 8 + .../src/test/test_qubino_flush_thermostat.lua | 8 + .../src/test/test_zwave_thermostat.lua | 8 + .../src/test/test_fibaro_roller_shutter.lua | 2 +- .../src/test/test_qubino_flush_shutter.lua | 2 +- .../test_zwave_iblinds_window_treatment.lua | 8 +- .../test_zwave_springs_window_treatment.lua | 2 +- .../src/test/test_zwave_window_treatment.lua | 2 +- 124 files changed, 2038 insertions(+), 1190 deletions(-) create mode 100644 drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua 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 1fffd78597..9aa45f7951 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -70,34 +70,29 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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 = "cook-surface-one-tl-cook-surface-two-tl" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Verify device profile update", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "cook-surface-one-tl-cook-surface-two-tl" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - test.register_coroutine_test( "Assert component to endpoint map", function() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua index f35a88c012..c6923c3de4 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua @@ -57,6 +57,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -71,7 +73,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -79,12 +80,13 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ profile = "dishwasher-tn-tl" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end 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 8d02396191..91339b78ca 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -86,6 +85,8 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.fanMode.ID] = { clusters.FanControl.attributes.FanModeSequence, @@ -117,16 +118,16 @@ local function test_init() end end end - - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) 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 = "extractor-hood-hepa-ac-wind" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_onoff() + test.disable_startup_messages() local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, clusters.FanControl.attributes.FanMode, @@ -135,15 +136,16 @@ local function test_init_onoff() clusters.FanControl.attributes.WindSetting, clusters.OnOff.attributes.OnOff } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_onoff) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(mock_device_onoff)) end end - test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.mock_device.add_test_device(mock_device_onoff) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) + test.socket.matter:__expect_send({mock_device_onoff.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure"}) mock_device_onoff:expect_metadata_update({ profile = "extractor-hood-wind-light" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 7695c51d48..10e0de0f5e 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -56,6 +56,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -76,8 +78,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 966d1f7d17..0334bf61cb 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua @@ -56,6 +56,8 @@ local mock_device_washer = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_washer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -69,7 +71,6 @@ local function test_init() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -77,8 +78,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_washer) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 57ec49cfbf..8f9a8c81d8 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 @@ -268,6 +268,8 @@ local mock_device_refrigerator = test.mock_device.build_test_matter_device({ }) local function test_init_dishwasher() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_dishwasher) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.DishwasherMode.attributes.CurrentMode, @@ -282,7 +284,6 @@ local function test_init_dishwasher() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dishwasher) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -290,11 +291,21 @@ local function test_init_dishwasher() end end test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_dishwasher) test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dishwasher.id, subscribe_request }) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dishwasher.id, read_req}) + test.socket.device_lifecycle:__queue_receive({ mock_device_dishwasher.id, "doConfigure"}) + mock_device_dishwasher:expect_metadata_update({ profile = "dishwasher-tn-tl" }) + mock_device_dishwasher:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -local function test_init_washer_dryer() +local function test_init_dryer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_dryer) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LaundryWasherMode.attributes.CurrentMode, @@ -308,29 +319,62 @@ local function test_init_washer_dryer() clusters.TemperatureControl.attributes.SelectedTemperatureLevel, clusters.TemperatureControl.attributes.SupportedTemperatureLevels } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_dryer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_dryer)) end end - local subscribe_request_washer = cluster_subscribe_list[1]:subscribe(mock_device_washer) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_dryer.id, read_req}) + mock_device_dryer:expect_metadata_update({ profile = "laundry-dryer-tn-tl" }) + mock_device_dryer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_washer() + test.disable_startup_messages() + test.socket.matter:__set_channel_ordering("relaxed") + test.mock_device.add_test_device(mock_device_washer) + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LaundryWasherMode.attributes.CurrentMode, + clusters.LaundryWasherMode.attributes.SupportedModes, + clusters.OperationalState.attributes.OperationalState, + clusters.OperationalState.attributes.OperationalError, + clusters.OperationalState.attributes.AcceptedCommandList, + clusters.TemperatureControl.attributes.TemperatureSetpoint, + clusters.TemperatureControl.attributes.MaxTemperature, + clusters.TemperatureControl.attributes.MinTemperature, + clusters.TemperatureControl.attributes.SelectedTemperatureLevel, + clusters.TemperatureControl.attributes.SupportedTemperatureLevels + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_washer) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request_washer:merge(cluster:subscribe(mock_device_washer)) + subscribe_request:merge(cluster:subscribe(mock_device_washer)) end end - test.socket.matter:__expect_send({ mock_device_dryer.id, subscribe_request }) - test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request_washer }) - test.mock_device.add_test_device(mock_device_dryer) - test.mock_device.add_test_device(mock_device_washer) - test.socket.device_lifecycle:__queue_receive({ mock_device_dryer.id, "added" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "init" }) + test.socket.matter:__expect_send({ mock_device_washer.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_washer.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_washer.id, read_req}) + mock_device_washer:expect_metadata_update({ profile = "laundry-washer-tn-tl" }) + mock_device_washer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_oven() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_oven) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -342,7 +386,6 @@ local function test_init_oven() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_oven) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -350,12 +393,16 @@ local function test_init_oven() end end test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_oven) test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "init" }) + test.socket.matter:__expect_send({ mock_device_oven.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_oven.id, "doConfigure"}) + mock_device_oven:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_refrigerator() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_refrigerator) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -365,7 +412,6 @@ local function test_init_refrigerator() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_refrigerator) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -373,14 +419,19 @@ local function test_init_refrigerator() end end test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_refrigerator) test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "added" }) - test.set_rpc_version(5) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "init" }) + test.socket.matter:__expect_send({ mock_device_refrigerator.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_refrigerator.id, "doConfigure"}) + local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() + read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) + test.socket.matter:__expect_send({mock_device_refrigerator.id, read_req}) + mock_device_refrigerator:expect_metadata_update({ profile = "refrigerator-freezer-tn-tl" }) + mock_device_refrigerator:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -420,7 +471,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for dishwasher, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dishwasher.id, @@ -460,7 +510,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -495,12 +544,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 28 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry washer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_washer.id, @@ -535,12 +583,11 @@ test.register_coroutine_test( { mock_device_washer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_washer, washer_ep, 50 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_washer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -575,12 +622,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for laundry dryer, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_dryer.id, @@ -615,12 +661,11 @@ test.register_coroutine_test( { mock_device_dryer.id, clusters.TemperatureControl.commands.SetTemperature(mock_device_dryer, dryer_ep, 40 * 100, nil) } ) end, - { test_init = test_init_washer_dryer } + { test_init = test_init_dryer } ) test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -660,7 +705,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for oven, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_oven.id, @@ -700,7 +744,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -740,7 +783,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for refrigerator endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -780,7 +822,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, @@ -820,7 +861,6 @@ test.register_coroutine_test( test.register_coroutine_test( "temperatureSetpoint command should send appropriate commands for freezer endpoint, temp bounds out of range and temp setpoint converted from F to C", function() - test.socket.capability:__set_channel_ordering("relaxed") test.socket.matter:__queue_receive( { mock_device_refrigerator.id, 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 3d7008b036..3c2e1fbc51 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -54,6 +54,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OperationalState.attributes.OperationalState, clusters.OperationalState.attributes.OperationalError, @@ -63,17 +65,19 @@ local function test_init() clusters.MicrowaveOvenControl.attributes.MaxCookTime, clusters.MicrowaveOvenControl.attributes.CookTime } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster: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.matter:__expect_send({ mock_device.id, clusters.MicrowaveOvenControl.attributes.MaxCookTime:read( mock_device, APPLICATION_ENDPOINT) }) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function init_supported_microwave_oven_modes() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua index 6af4556c6a..58c38e078c 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -112,6 +112,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -123,7 +125,6 @@ local function test_init() clusters.OvenMode.attributes.CurrentMode, clusters.OvenMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -131,8 +132,11 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua index 1ef5734d0f..3dc19750f3 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua @@ -67,6 +67,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.RefrigeratorAlarm.attributes.State, clusters.RefrigeratorAndTemperatureControlledCabinetMode.attributes.CurrentMode, @@ -76,7 +78,6 @@ local function test_init() clusters.TemperatureControl.attributes.MinTemperature, clusters.TemperatureMeasurement.attributes.MeasuredValue } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -84,8 +85,9 @@ local function test_init() end end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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"}) local read_req = clusters.TemperatureControl.attributes.MinTemperature:read() read_req:merge(clusters.TemperatureControl.attributes.MaxTemperature:read()) 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 7655c29e1e..a9d2629803 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 @@ -74,6 +74,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ } local function test_init() + test.set_rpc_version(0) 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 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 42d97c109d..2ecf083ba7 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 @@ -73,35 +73,47 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added results in a profile update + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + mock_device:expect_metadata_update({ profile = "4-button-battery" }) + + -- init results in subscription interaction 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - mock_device:expect_metadata_update({ profile = "4-button-battery" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioing state to provisioned + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + -- simulate the profile change update taking affect and the device info changing local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "4-buttons-battery" local device_info_json = dkjson.encode(device_info_copy) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) end test.set_test_init_function(test_init) +-- this one is failing because it expects added or test.register_message_test( "Handle single press sequence, no hold", { { 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 453858d7c2..27f8585185 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -62,6 +62,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, @@ -69,16 +71,15 @@ local function test_init() clusters.PowerSource.attributes.BatPercentRemaining, clusters.PowerSource.attributes.BatChargeState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.matter:__expect_send({ mock_device.id, @@ -90,6 +91,8 @@ local function test_init() clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse.lua b/drivers/SmartThings/matter-energy/src/test/test_evse.lua index ad9db31150..6add71bde9 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse.lua @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -90,19 +92,22 @@ local function test_init() clusters.DeviceEnergyManagementMode.attributes.CurrentMode, clusters.DeviceEnergyManagementMode.attributes.SupportedModes, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-power-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 91332241b7..84c431ffe2 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 @@ -75,6 +75,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.EnergyEvse.attributes.State, clusters.EnergyEvse.attributes.SupplyState, @@ -89,16 +91,15 @@ local function test_init() clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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.matter:__expect_send({ mock_device.id, @@ -107,6 +108,10 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ profile = "evse-energy-meas" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 049b364d6f..87fde17a23 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -71,21 +71,22 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end - test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.mock_device.add_test_device(mock_device) 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 }) local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) @@ -93,6 +94,9 @@ local function test_init() mock_device.id, read_req }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 b64324d8f7..7427ad31d0 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 @@ -41,7 +41,8 @@ local mock_device = test.mock_device.build_test_matter_device({ cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 0x0001, --u32 bitmap + feature_map = clusters.DoorLock.types.Feature.PIN_CREDENTIAL | + clusters.DoorLock.types.Feature.USER } }, device_types = { @@ -52,6 +53,13 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) @@ -63,7 +71,12 @@ local function test_init() subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm: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.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" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 3c95216dc5..41ecbd612e 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 @@ -15,6 +15,7 @@ local test = require "integration_test" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local mock_device_record = { @@ -41,58 +42,75 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local mock_device_record_level = { - profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), - manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus - endpoints = { - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } + profile = t_utils.get_profile_definition("lock-nocodes-notamper-batteryLevel.yml"), + manufacturer_info = {vendor_id = 0x129F, product_id = 0x0001}, -- Level Lock Plus + endpoints = { + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, - { - endpoint_id = 10, - clusters = { - {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, - }, + device_types = { + device_type_id = 0x0016, device_type_revision = 1, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + {cluster_id = clusters.DoorLock.ID, cluster_type = "SERVER", feature_map = 0x0000}, }, }, + }, } local mock_device_level = test.mock_device.build_test_matter_device(mock_device_record_level) local function test_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.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes("[]", {visibility = {displayed = false}})) + ) + local req = clusters.DoorLock.attributes.MaxPINCodeLength:read(mock_device, 10) + req:merge(clusters.DoorLock.attributes.MinPINCodeLength:read(mock_device, 10)) + req:merge(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device, 10)) + 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) + - local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) - test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) - test.mock_device.add_test_device(mock_device_level) + 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) + test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) end test.set_test_init_function(test_init) test.register_coroutine_test( - "doConfigure lifecycle event for base-lock-nobattery", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for base-lock-nobattery", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.register_coroutine_test( - "doConfigure lifecycle event for Level Lock Plus profile", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) - mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + "doConfigure lifecycle event for Level Lock Plus profile", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "doConfigure" }) + mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end ) test.run_registered_tests() 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 7701d5a90c..0123d43239 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -43,12 +43,21 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 6aff133939..92af7fcabc 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 @@ -15,6 +15,7 @@ local test = require "integration_test" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" @@ -67,28 +68,41 @@ local mock_device_no_battery_record = { local mock_device_no_battery = test.mock_device.build_test_matter_device(mock_device_no_battery_record) local function test_init() + test.disable_startup_messages() + 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.tamperAlert.tamper.clear()) + ) + mock_device:expect_metadata_update({ profile = "lock-without-codes" }) + 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.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) local function test_init_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_no_battery) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "added" }) + test.socket.capability:__expect_send( + mock_device_no_battery:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + ) + mock_device_no_battery:expect_metadata_update({ profile = "lock-without-codes-nobattery" }) + 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)) + 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.mock_device.add_test_device(mock_device_no_battery) test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "doConfigure" }) mock_device_no_battery:expect_metadata_update({ profile = "base-lock-nobattery" }) mock_device_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 22f9ccd29a..d76556a42d 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 @@ -44,10 +44,15 @@ local mock_device = test.mock_device.build_test_matter_device(mock_device_record local function test_init() - local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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)) + 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" }) end test.set_test_init_function(test_init) @@ -84,20 +89,20 @@ test.register_message_test( message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.warning()), }, { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( - mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK - ), - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device, 10, clusters.PowerSource.types.BatChargeLevelEnum.OK + ), }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.batteryLevel.battery.normal()), + }, } ) 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 c672410296..ad68ffbe3b 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 @@ -12,20 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- Copyright 2022 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 capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") @@ -35,6 +21,7 @@ local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock local im = require "st.matter.interaction_model" local types = DoorLock.types + local mock_device_record = { profile = t_utils.get_profile_definition("base-lock.yml"), manufacturer_info = {vendor_id = 0xcccc, product_id = 0x1}, @@ -56,7 +43,7 @@ local mock_device_record = { cluster_type = "SERVER", feature_map = 0x0101, -- PIN & USR }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, }, }, @@ -64,13 +51,18 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "base-lock-nobattery" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -189,11 +181,11 @@ local function init_code_slot(slot_number, name, device) ) local credential = DoorLock.types.DlCredential( - { + { credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = slot_number, } - ) + ) test.socket.matter:__expect_send( { device.id, 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 6303ec7bcd..f25fe2a19c 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 @@ -12,20 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. --- Copyright 2022 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 capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") @@ -64,6 +50,9 @@ local mock_device_record = { local mock_device = test.mock_device.build_test_matter_device(mock_device_record) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + 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)) @@ -71,7 +60,9 @@ local function test_init() 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) end test.set_test_init_function(test_init) 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 2e390f4d34..92adc6c6fd 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 @@ -54,28 +54,28 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "lock-unlatch" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-unlatch" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() 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 9659c9484d..442593b188 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 @@ -55,6 +55,13 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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)) @@ -68,23 +75,16 @@ local function test_init() 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.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "lock-user-pin" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) end test.set_test_init_function(test_init) -test.register_coroutine_test( - "Assert profile applied over doConfigure", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-user-pin" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - end -) - test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function() 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 603f056c06..d8657ccae3 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 @@ -174,24 +174,55 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter }) local function test_init() + test.disable_startup_messages() + 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})) + ) + 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.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.mock_device.add_test_device(mock_device) + 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}})) + ) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_unlatch() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) test.socket["matter"]:__expect_send({mock_device_unlatch.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_user_pin) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) @@ -203,10 +234,22 @@ local function test_init_user_pin() subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) test.socket["matter"]:__expect_send({mock_device_user_pin.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_user_pin) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin_schedule_unlatch() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) @@ -220,7 +263,12 @@ local function test_init_user_pin_schedule_unlatch() subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) test.socket["matter"]:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) @@ -228,18 +276,6 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Test lock profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -264,18 +300,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -301,18 +325,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, @@ -339,18 +351,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -376,18 +376,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -414,18 +402,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) - mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_unlatch.id, @@ -453,18 +429,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -490,18 +454,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -528,18 +480,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) - mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin.id, @@ -567,18 +507,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when attributes related to BAT feature is not available.", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -604,18 +532,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel attribute is available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, @@ -642,18 +558,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test lock-user-pin-schedule-unlatch profile change when BatChargeLevel and BatPercentRemaining attributes are available", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) - mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - ) - test.socket.matter:__expect_send( - { - mock_device_user_pin_schedule_unlatch.id, - clusters.PowerSource.attributes.AttributeList:read() - } - ) - test.wait_for_events() test.socket.matter:__queue_receive( { mock_device_user_pin_schedule_unlatch.id, 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 5444652b29..cf5bc44648 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -49,286 +48,283 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) - local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel - } - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device) + subscribe_request:merge(clusters.LevelControl.attributes.CurrentLevel: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" }) end test.set_test_init_function(test_init) test.register_message_test( - "Mute and unmute commands should send the appropriate commands", + "Mute and unmute commands should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "mute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "unmute", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "mute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "unmute", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.unmuted()) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 10, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioMute.mute.muted()) + } + } ) test.register_message_test( - "Set mute command should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.Off(mock_device, 10) - } - }, - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 10) - } - } + "Set mute command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "muted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 10) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioMute", component = "main", command = "setMute", args = { "unmuted" } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 10) + } } + } ) test.register_message_test( - "Set volume command should send the appropriate commands", + "Set volume command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) } + } ) test.register_message_test( - "Volume up/down command should send the appropriate commands", + "Volume up/down command should send the appropriate commands", + { { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - -- volume up - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) - }, - -- volume down - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) - }, - } + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "setVolume", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + -- volume up + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeUp", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(25/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 63 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(25)) + }, + -- volume down + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "audioVolume", component = "main", command = "volumeDown", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 10, math.floor(20/100.0 * 254), 0, 0, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 10) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 50 ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.audioVolume.volume(20)) + }, + } ) local function refresh_commands(dev) @@ -338,25 +334,24 @@ local function refresh_commands(dev) end test.register_message_test( - "Handle received refresh.", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "refresh", component = "main", command = "refresh", args = { } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - refresh_commands(mock_device) - } - }, - } + "Handle received refresh.", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + refresh_commands(mock_device) + } + }, + } ) - 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 52cde71ed3..ebae93ca53 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -84,34 +83,88 @@ local mock_device_variable_speed = test.mock_device.build_test_matter_device({ } }) +local supported_key_codes = { + "UP", + "DOWN", + "LEFT", + "RIGHT", + "SELECT", + "BACK", + "EXIT", + "MENU", + "SETTINGS", + "HOME", + "NUMBER0", + "NUMBER1", + "NUMBER2", + "NUMBER3", + "NUMBER4", + "NUMBER5", + "NUMBER6", + "NUMBER7", + "NUMBER8", + "NUMBER9" +} local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.MediaPlayback.attributes.CurrentState } - test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device)) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) + + test.mock_device.add_test_device(mock_device_variable_speed) + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "init" }) subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_variable_speed) - for i, cluster in ipairs(cluster_subscribe_list) do - print(i) - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_variable_speed)) - end - print(subscribe_request) - end + subscribe_request:merge(cluster_subscribe_list[2]:subscribe(mock_device_variable_speed)) test.socket.matter:__expect_send({mock_device_variable_speed.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_variable_speed) + + test.socket.device_lifecycle:__queue_receive({ mock_device_variable_speed.id, "doConfigure" }) + mock_device_variable_speed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaPlayback.supportedPlaybackCommands({"play", "pause", "stop", "rewind", "fastForward"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.mediaTrackControl.supportedTrackControlCommands({"previousTrack", "nextTrack"}) + ) + ) + test.socket.capability:__expect_send( + mock_device_variable_speed:generate_test_message( + "main", capabilities.keypadInput.supportedKeyCodes(supported_key_codes) + ) + ) end test.set_test_init_function(test_init) @@ -557,28 +610,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) @@ -608,28 +640,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_variable_speed:generate_test_message( "main", - capabilities.keypadInput.supportedKeyCodes({ - "UP", - "DOWN", - "LEFT", - "RIGHT", - "SELECT", - "BACK", - "EXIT", - "MENU", - "SETTINGS", - "HOME", - "NUMBER0", - "NUMBER1", - "NUMBER2", - "NUMBER3", - "NUMBER4", - "NUMBER5", - "NUMBER6", - "NUMBER7", - "NUMBER8", - "NUMBER9", - }) + capabilities.keypadInput.supportedKeyCodes(supported_key_codes) ) ) diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index c8fae5aaad..4165535fef 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -16,9 +16,6 @@ local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local area_type = require "Global.types.AreaTypeTag" -local landmark = require "Global.types.LandmarkTag" - local embedded_cluster_utils = require "embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 10 @@ -512,15 +509,15 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp if location_info.location_name.value ~= "" then area_name = location_info.location_name.value elseif location_info.floor_number.value ~= nil and location_info.area_type.value ~= nil then - area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = location_info.floor_number.value .. "F " .. upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) elseif location_info.floor_number.value ~= nil then area_name = location_info.floor_number.value .. "F" elseif location_info.area_type.value ~= nil then - area_name = upper_to_camelcase(string.gsub(area_type.pretty_print(location_info.area_type),"AreaTypeTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.AreaTypeTag.pretty_print(location_info.area_type),"AreaTypeTag: ","")) end end if area_name == "" then - area_name = upper_to_camelcase(string.gsub(landmark.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) + area_name = upper_to_camelcase(string.gsub(clusters.Global.types.LandmarkTag.pretty_print(landmark_info.landmark_tag),"LandmarkTag: ","")) end table.insert(supported_areas, {["areaId"] = area_id, ["areaName"] = area_name}) end 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 1fb2afb752..5bed191802 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -64,6 +64,8 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.mode.ID] = { clusters.RvcRunMode.attributes.SupportedModes, @@ -90,9 +92,14 @@ local function test_init() end end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) 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 = "rvc-clean-mode-service-area" }) + test.socket.matter:__expect_send({mock_device.id, clusters.RvcOperationalState.attributes.AcceptedCommandList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 b2b8ae4ebf..871aec64e2 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 @@ -249,6 +249,9 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at test.mock_device.add_test_device(generic_mock_device) end +-- TODO add tests for configuration using modular profiles +test.set_rpc_version(7) + local function test_init() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { 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 880ed55c89..f6312dfc8f 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 @@ -53,6 +53,7 @@ local subscribed_attributes = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then @@ -60,8 +61,6 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) end test.set_test_init_function(test_init) 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 46bc88a771..7aae047a29 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 @@ -19,73 +19,62 @@ local clusters = require "st.matter.clusters" clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" local mock_device_freeze_leak = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, + profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1} -- RootNode + } }, - 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.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector - } + device_types = { + {device_type_id = 0x0043, device_type_revision = 1} -- Water Leak Detector + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, }, - { - endpoint_id = 2, - clusters = { - {cluster_id = clusters.BooleanState.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.BooleanStateConfiguration.ID, cluster_type = "SERVER", feature_map = 31}, - }, - device_types = { - {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector - } + device_types = { + {device_type_id = 0x0041, device_type_revision = 1} -- Water Freeze Detector } } + } }) -local subscribed_attributes = { - clusters.BooleanState.attributes.StateValue, - clusters.BooleanStateConfiguration.attributes.SensorFault, -} - local function test_init_freeze_leak() - local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_freeze_leak) - for i, cluster in ipairs(subscribed_attributes) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_freeze_leak)) - end - end + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_freeze_leak) + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "init" }) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 1)}) test.socket.matter:__expect_send({mock_device_freeze_leak.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_freeze_leak, 2)}) + local subscribe_request = clusters.BooleanState.attributes.StateValue:subscribe(mock_device_freeze_leak) + subscribe_request:merge(clusters.BooleanStateConfiguration.attributes.SensorFault:subscribe(mock_device_freeze_leak)) test.socket.matter:__expect_send({mock_device_freeze_leak.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_freeze_leak) + + test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) + mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) + mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_freeze_leak) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_freeze_leak.id, "doConfigure" }) - mock_device_freeze_leak:expect_metadata_update({ profile = "freeze-leak-fault-freezeSensitivity-leakSensitivity" }) - mock_device_freeze_leak:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_freeze_leak } -) - test.register_message_test( "Boolean state freeze detection reports should generate correct messages", { 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 27d3b90842..68134ed677 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 @@ -49,17 +49,11 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoints = matter_endpoints }) -local function subscribe_on_init(dev) - local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) - subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - return subscribe_request -end - local function test_init() - test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device since we are just testing the "pressure-battery" profile specifically - mock_device:set_field("__battery_checked", 1, {persist = true}) + local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) 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 b23f72de53..835de811ae 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 @@ -55,28 +55,24 @@ local subscribed_attributes = { } local function test_init_rain() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_rain) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device_rain) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device_rain)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "init" }) test.socket.matter:__expect_send({mock_device_rain.id, clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(mock_device_rain, 1)}) test.socket.matter:__expect_send({mock_device_rain.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_rain) + + test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) + mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) + mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init_rain) -test.register_coroutine_test( - "Test profile change on init for Freeze and Leak combined device type", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_rain.id, "doConfigure" }) - mock_device_rain:expect_metadata_update({ profile = "rain-fault-rainSensitivity" }) - mock_device_rain:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { test_init = test_init_rain } -) - test.register_message_test( "Boolean state rain detection reports should generate correct messages", { 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 5bed934846..7116a03b92 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua @@ -118,7 +118,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - test.set_rpc_version(5) end test.set_test_init_function(test_init) @@ -134,8 +133,10 @@ local function subscribe_on_init_presence_sensor(dev) end local function test_init_presence_sensor() - test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_presence_sensor) + test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "init" }) + test.socket.matter:__expect_send({mock_device_presence_sensor.id, subscribe_on_init_presence_sensor(mock_device_presence_sensor)}) test.socket.device_lifecycle:__queue_receive({ mock_device_presence_sensor.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_presence_sensor.id, read_attribute_list}) 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 48b72e232c..72484efffe 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 @@ -56,20 +56,21 @@ local cluster_subscribe_list_humidity_battery = { } local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 91d795d885..ed9718b947 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 @@ -125,52 +125,52 @@ local cluster_subscribe_list_temp_humidity = { } local function test_init_humidity_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_battery) local subscribe_request_humidity_battery = cluster_subscribe_list_humidity_battery[1]:subscribe(mock_device_humidity_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_battery) do if i > 1 then subscribe_request_humidity_battery:merge(cluster:subscribe(mock_device_humidity_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_battery.id, subscribe_request_humidity_battery}) - test.mock_device.add_test_device(mock_device_humidity_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_battery.id, "doConfigure" }) - mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device_humidity_battery.id, read_attribute_list}) + mock_device_humidity_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_humidity_no_battery() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_humidity_no_battery) local subscribe_request_humidity_no_battery = cluster_subscribe_list_humidity_no_battery[1]:subscribe(mock_device_humidity_no_battery) for i, cluster in ipairs(cluster_subscribe_list_humidity_no_battery) do if i > 1 then subscribe_request_humidity_no_battery:merge(cluster:subscribe(mock_device_humidity_no_battery)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "init" }) test.socket.matter:__expect_send({mock_device_humidity_no_battery.id, subscribe_request_humidity_no_battery}) - test.mock_device.add_test_device(mock_device_humidity_no_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_humidity_no_battery.id, "doConfigure" }) mock_device_humidity_no_battery:expect_metadata_update({ profile = "humidity" }) mock_device_humidity_no_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_temp_humidity() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_temp_humidity) local subscribe_request_temp_humidity = cluster_subscribe_list_temp_humidity[1]:subscribe(mock_device_temp_humidity) for i, cluster in ipairs(cluster_subscribe_list_temp_humidity) do if i > 1 then subscribe_request_temp_humidity:merge(cluster:subscribe(mock_device_temp_humidity)) end end - + test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "init" }) test.socket.matter:__expect_send({mock_device_temp_humidity.id, subscribe_request_temp_humidity}) - test.mock_device.add_test_device(mock_device_temp_humidity) - test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_temp_humidity.id, "doConfigure" }) mock_device_temp_humidity:expect_metadata_update({ profile = "temperature-humidity" }) mock_device_temp_humidity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 d8b38e392a..7eb31d5c29 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 @@ -57,8 +57,6 @@ end local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_on_init(mock_device)}) test.mock_device.add_test_device(mock_device) - -- don't check the battery for this device because we are using the catch-all "sensor.yml" profile just for testing - mock_device:set_field("__battery_checked", 1, {persist = true}) test.set_rpc_version(3) end test.set_test_init_function(test_init) 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 f5029147d7..86060ab442 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 @@ -27,6 +27,7 @@ end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("smoke-co-temp-humidity-comeas.yml"), + _provisioning_state = "TYPED", -- we want this to have doConfigure on startup manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -73,6 +74,10 @@ local cluster_subscribe_list = { } local function test_init() + -- The startup messages are enabled, so this device will get an init, + -- and doConfigure (because provisioning_state is TYPED on the device). + test.mock_device.add_test_device(mock_device) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -80,11 +85,9 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) 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 9bcdc54e44..b618b3e5d6 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 @@ -72,18 +72,20 @@ local cluster_subscribe_list = { } 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, cluster in ipairs(cluster_subscribe_list) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + 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" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) 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 830a9b99f8..d2e99b2331 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 @@ -18,7 +18,6 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button @@ -121,6 +120,8 @@ local function configure_buttons() end local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.TemperatureMeasurement.attributes.MeasuredValue, @@ -140,17 +141,17 @@ local function test_init() end end + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) configure_buttons() aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) - test.set_rpc_version(5) - - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) device_info_copy.profile.id = "3-button-battery-temperature-humidity" 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 369689e181..29544d0e8d 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 @@ -17,12 +17,9 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button -local DEFERRED_CONFIGURE = "__DEFERRED_CONFIGURE" - local aqara_parent_ep = 4 local aqara_child1_ep = 1 local aqara_child2_ep = 2 @@ -159,7 +156,8 @@ local function configure_buttons() end local function test_init() - local opts = { persist = true } + test.disable_startup_messages() + test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.Switch.server.events.InitialPress, @@ -176,13 +174,16 @@ local function test_init() subscribe_request:merge(cluster:subscribe(aqara_mock_device)) end end + + -- Test added -> doConfigure logic + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) + test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "init" }) test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - test.mock_devices_api._expected_device_updates[aqara_mock_device.device_id] = "00000000-1111-2222-3333-000000000001" - test.mock_devices_api._expected_device_updates[1] = {device_id = "00000000-1111-2222-3333-000000000001"} - test.mock_devices_api._expected_device_updates[1].metadata = {deviceId="00000000-1111-2222-3333-000000000001", profileReference="4-button"} + configure_buttons() + aqara_mock_device:expect_metadata_update({ profile = "4-button" }) aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(aqara_mock_device) -- to test powerConsumptionReport test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") @@ -206,11 +207,6 @@ local function test_init() parent_assigned_child_key = string.format("%d", aqara_child2_ep) }) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "added" }) - configure_buttons() - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - - aqara_mock_device:set_field(DEFERRED_CONFIGURE, true, opts) local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) device_info_copy.profile.id = "4-button" local device_info_json = dkjson.encode(device_info_copy) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index bf791d906b..d9759b7add 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -14,12 +14,14 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local version = require "version" -clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" -clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), @@ -56,7 +58,7 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug } - }, + } }, }) 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 f34f432ec7..23023b3d02 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -58,19 +58,24 @@ local function configure_buttons() end 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 + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) configure_buttons() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "buttons-battery" local device_info_json = dkjson.encode(device_info_copy) 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 7d3211bdfb..078b99c7fe 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 @@ -84,12 +84,19 @@ local CLUSTER_SUBSCRIBE_LIST ={ } 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 + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + + 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 = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 624a3ab205..d8b0116101 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 @@ -99,7 +99,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) @@ -116,28 +116,39 @@ local function configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init 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.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" }) - test.mock_device.add_test_device(mock_device) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing local device_info_copy = utils.deep_copy(mock_device.raw_st_data) device_info_copy.profile.id = "5-buttons-battery" local device_info_json = dkjson.encode(device_info_copy) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() + expect_configure_buttons() end - test.set_test_init_function(test_init) test.register_message_test( 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 139c519dbf..af7511100e 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 @@ -181,7 +181,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.Switch.server.events.MultiPressComplete, } -local function configure_buttons() +local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) @@ -192,23 +192,29 @@ local function configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + test.mock_device.add_test_device(mock_child) + + -- added sets a bunch of fields on the device, and calls init 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "3-button" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - configure_buttons() - test.mock_device.add_test_device(mock_device) - test.mock_device.add_test_device(mock_child) mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -216,37 +222,30 @@ local function test_init() parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", mock_device_ep5) }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - configure_buttons() + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() + + test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) + mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) end +-- All messages queued and expectations set are done before the driver is actually run local function test_init_mcd_unsupported_switch_device_type() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) - end - end - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - mock_device_mcd_unsupported_switch_device_type:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 1", - profile = "switch-binary", - parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, - parent_assigned_child_key = string.format("%d", 7) - }) - test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_mcd_unsupported_switch_device_type) -- make sure the cache is populated end test.set_test_init_function(test_init) @@ -404,7 +403,49 @@ test.register_message_test( test.register_coroutine_test( "Test MCD configuration not including switch for unsupported switch device type, create child device instead", function() - end, + -- added sets a bunch of fields on the device, and calls init + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) + end + end + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "added" }) + test.wait_for_events() + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "init" }) + test.wait_for_events() + + -- doConfigure sets the provisioning state to provisioned + mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) + mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + mock_device_mcd_unsupported_switch_device_type:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 1", + profile = "switch-binary", + parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, + parent_assigned_child_key = string.format("%d", 7) + }) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) + test.wait_for_events() + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device_mcd_unsupported_switch_device_type.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + end, { test_init = test_init_mcd_unsupported_switch_device_type } ) @@ -413,7 +454,7 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) - configure_buttons() + expect_configure_buttons() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index a35552007a..0046244bcd 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -1,8 +1,23 @@ +-- Copyright 2025 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 test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +test.disable_startup_messages() + local mock_device_onoff = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { @@ -406,15 +421,19 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device }) local function test_init_parent_child_switch_types() + test.mock_device.add_test_device(mock_device_parent_child_switch_types) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_switch_types) - mock_device_parent_child_switch_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -426,6 +445,8 @@ end local function test_init_onoff() test.mock_device.add_test_device(mock_device_onoff) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_onoff.id, "doConfigure" }) mock_device_onoff:expect_metadata_update({ profile = "switch-binary" }) mock_device_onoff:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -436,12 +457,18 @@ local function test_init_onoff_client() end local function test_init_parent_client_child_server() + test.mock_device.add_test_device(mock_device_parent_client_child_server) local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_client_child_server:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_client_child_server) end local function test_init_dimmer() @@ -453,12 +480,15 @@ end local function test_init_color_dimmer() test.mock_device.add_test_device(mock_device_color_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_color_dimmer.id, "doConfigure" }) mock_device_color_dimmer:expect_metadata_update({ profile = "switch-color-level" }) mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_mounted_on_off_control() + test.mock_device.add_test_device(mock_device_mounted_on_off_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } @@ -468,13 +498,18 @@ local function test_init_mounted_on_off_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_on_off_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "added" }) + test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "init" }) test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_on_off_control) end local function test_init_mounted_dimmable_load_control() + test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } @@ -484,20 +519,27 @@ local function test_init_mounted_dimmable_load_control() subscribe_request:merge(cluster:subscribe(mock_device_mounted_dimmable_load_control)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "added" }) test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "init" }) + test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end local function test_init_water_valve() test.mock_device.add_test_device(mock_device_water_valve) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_water_valve.id, "doConfigure" }) mock_device_water_valve:expect_metadata_update({ profile = "water-valve-level" }) mock_device_water_valve:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_child_different_types() + test.mock_device.add_test_device(mock_device_parent_child_different_types) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -517,13 +559,15 @@ local function test_init_parent_child_different_types() subscribe_request:merge(cluster:subscribe(mock_device_parent_child_different_types)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_different_types) - mock_device_parent_child_different_types:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", @@ -534,10 +578,12 @@ local function test_init_parent_child_different_types() end local function test_init_parent_child_unsupported_device_type() + test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_unsupported_device_type) mock_device_parent_child_unsupported_device_type:expect_device_create({ type = "EDGE_CHILD", @@ -549,6 +595,7 @@ local function test_init_parent_child_unsupported_device_type() end local function test_init_light_level_motion() + test.mock_device.add_test_device(mock_device_light_level_motion) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -562,8 +609,15 @@ local function test_init_light_level_motion() subscribe_request:merge(cluster:subscribe(mock_device_light_level_motion)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "added" }) test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_light_level_motion) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "init" }) + test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index de62865597..21c9e1087d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -15,8 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" + +test.disable_startup_messages() + local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 local OPTIONS_OVERRIDE = 0x01 @@ -159,6 +161,7 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -178,12 +181,16 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -225,6 +232,7 @@ for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endp end local function test_init_parent_child_endpoints_non_sequential() + test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -244,12 +252,16 @@ local function test_init_parent_child_endpoints_non_sequential() subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) end end + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index cbf8fb0afe..03c898b7f5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -15,9 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" - local clusters = require "st.matter.clusters" +test.disable_startup_messages() + local child_profile = t_utils.get_profile_definition("plug-binary.yml") local child_profile_override = t_utils.get_profile_definition("switch-binary.yml") local parent_ep = 10 @@ -132,16 +133,21 @@ for i, endpoint in ipairs(mock_device.endpoints) do end local function test_init() + test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device) for _, child in pairs(mock_children) do test.mock_device.add_test_device(child) end @@ -177,16 +183,21 @@ for i, endpoint in ipairs(mock_device_child_profile_override.endpoints) do end local function test_init_child_profile_override() + test.mock_device.add_test_device(mock_device_child_profile_override) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) + + test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "added" }) + test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "init" }) test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.mock_device.add_test_device(mock_device_child_profile_override) for _, child in pairs(mock_children_child_profile_override) do test.mock_device.add_test_device(child) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 398d3756c8..31f99f537d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -201,6 +201,8 @@ local function configure_buttons() 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 } @@ -208,13 +210,17 @@ local function test_init() 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() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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) 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 f05c5ee175..d9cb8c6309 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 @@ -12,6 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" 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 c21de741e2..e5f44e3289 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 @@ -11,15 +11,19 @@ -- 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 test = require "integration_test" -test.set_rpc_version(8) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" +test.disable_startup_messages() + if version.api < 10 then clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" @@ -224,6 +228,21 @@ local cluster_subscribe_list_configured = { } local function test_init_basic() + test.mock_device.add_test_device(mock_device_basic) + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then @@ -231,10 +250,25 @@ local function test_init_basic() end end test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_basic) end local function test_init_ap_thermo_aqs_preconfigured() + test.mock_device.add_test_device(mock_device_ap_thermo_aqs) + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.AttributeList, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_ap_thermo_aqs)) + end + test.socket.matter:__expect_send({ mock_device_ap_thermo_aqs.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "init" }) local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do for _, attribute in ipairs(attributes) do @@ -246,7 +280,6 @@ local function test_init_ap_thermo_aqs_preconfigured() end end test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_ap_thermo_aqs) end local expected_update_metadata= { @@ -283,11 +316,16 @@ local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_basic) test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "doConfigure" }) + test.socket.matter:__queue_receive({ + mock_device_basic.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_basic, 1, {uint32(0)}) + }) mock_device_basic:expect_metadata_update(expected_update_metadata) mock_device_basic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_basic.raw_st_data) device_info_copy.profile.id = "air-purifier-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -339,7 +377,6 @@ local expected_update_metadata= { local subscribe_request = nil for _, attributes in pairs(cluster_subscribe_list_configured) do - print("Adding attribute to subscribe", attributes) for _, attribute in ipairs(attributes) do if subscribe_request == nil then subscribe_request = attribute:subscribe(mock_device_ap_thermo_aqs) @@ -352,11 +389,17 @@ end test.register_coroutine_test( "Test profile change on init for AP and Thermo and AQS combined device type", function() - mock_device_ap_thermo_aqs:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_ap_thermo_aqs:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "doConfigure" }) - mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) mock_device_ap_thermo_aqs:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device_ap_thermo_aqs.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_ap_thermo_aqs, 1, {uint32(0)}) + }) + mock_device_ap_thermo_aqs:expect_metadata_update(expected_update_metadata) + + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_ap_thermo_aqs.raw_st_data) device_info_copy.profile.id = "air-purifier-modular" local device_info_json = dkjson.encode(device_info_copy) 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 3a33e94251..9af8c51982 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 @@ -83,6 +83,8 @@ local device_desc = { } local test_init_common = function(device) + test.disable_startup_messages() + test.mock_device.add_test_device(device) local cluster_subscribe_list = { clusters.Thermostat.attributes.SystemMode, clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -107,7 +109,6 @@ local test_init_common = function(device) subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({ device.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ device.id, "added" }) local read_request_on_added = { clusters.Thermostat.attributes.ControlSequenceOfOperation, @@ -124,7 +125,8 @@ local test_init_common = function(device) device.id, read_request }) - test.mock_device.add_test_device(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ device.id, subscribe_request }) end local mock_device = test.mock_device.build_test_matter_device(device_desc) @@ -181,6 +183,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -194,6 +201,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 23*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -213,6 +225,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_ONE_EP, 39*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatOne", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -226,6 +243,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, THERMOSTAT_TWO_EP, 19*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("thermostatTwo", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", 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 6c30fae787..cec0cb9ffc 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 @@ -12,10 +12,10 @@ -- See the License for the specific language governing permissions and -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -35,15 +35,15 @@ local mock_device = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, } + } } }) @@ -64,18 +64,18 @@ local mock_device_configure = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) 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 096ce71221..9be85cd84b 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 @@ -16,9 +16,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" +test.disable_startup_messages() test.set_rpc_version(8) local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -38,18 +40,18 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) @@ -70,22 +72,23 @@ local mock_device_no_state = test.mock_device.build_test_matter_device({ } }, { - endpoint_id = 1, - clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, - {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, - }, - device_types = { - {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner - } + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 63}, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER", feature_map = 0}, + }, + device_types = { + {device_type_id = 0x0072, device_type_revision = 1} -- Room Air Conditioner } + } } }) local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) + test.mock_device.add_test_device(generic_mock_device) local subscribe_request = nil for _, attributes in pairs(generic_subscribed_attributes) do for _, attribute in ipairs(attributes) do @@ -97,12 +100,27 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end +local function read_req_on_added(device) + local attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(attributes) do + read_request:merge(clus:read(device)) + end + test.socket.matter:__expect_send({ device.id, read_request }) +end + local subscribe_request_basic local function test_init_basic() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -145,6 +163,8 @@ local function test_init_basic() clusters.FanControl.attributes.WindSetting }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + read_req_on_added(mock_device_basic) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() test.socket.matter:__expect_send({mock_device_basic.id, read_setpoint_deadband}) @@ -204,8 +224,8 @@ for _, attributes in pairs(subscribed_attributes_no_state) do end end - local function test_init_no_state() + test.socket.matter:__set_channel_ordering("relaxed") local subscribed_attributes = { [capabilities.switch.ID] = { clusters.OnOff.attributes.OnOff @@ -249,6 +269,8 @@ local function test_init_no_state() }, } + test.socket.device_lifecycle:__queue_receive({ mock_device_no_state.id, "added" }) + read_req_on_added(mock_device_no_state) -- initially, device onboards WITH thermostatOperatingState, the test below will -- check if it is removed correctly when switching to modular profile. This is done -- to test that cases where the modular profile is different from the static profile @@ -260,10 +282,16 @@ local function test_init_no_state() end -- run the profile configuration tests -local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) +local function test_room_ac_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request, thermostat_attr_list_value) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {thermostat_attr_list_value}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) device_info_copy.profile.id = "room-air-conditioner-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -292,9 +320,7 @@ local expected_metadata_basic= { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - basic", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", true) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic) + test_room_ac_device_type_update_modular_profile(mock_device_basic, expected_metadata_basic, subscribe_request_basic, uint32(0x29)) end, { test_init = test_init_basic } ) @@ -319,9 +345,7 @@ local expected_metadata_no_state = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - no thermo state", function() - mock_device_no_state:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_no_state:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. - test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state) + test_room_ac_device_type_update_modular_profile(mock_device_no_state, expected_metadata_no_state, subscribe_request_no_state, uint32(0)) end, { test_init = test_init_no_state } ) 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 96fdf02f3d..35f312c67e 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 @@ -17,6 +17,8 @@ local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-batteryLevel.yml"), manufacturer_info = { 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 59ae9a433f..37f73d0e50 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 @@ -15,9 +15,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" +test.set_rpc_version(7) + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), manufacturer_info = { 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 d1a70c3f06..2086036be5 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 @@ -14,8 +14,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" +local dkjson = require "dkjson" +local uint32 = require "st.matter.data_types.Uint32" +local utils = require "st.utils" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -135,32 +137,18 @@ local cluster_subscribe_list = { clusters.FanControl.attributes.FanModeSequence, } -local cluster_subscribe_list_disorder_endpoints = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, - clusters.Thermostat.attributes.ControlSequenceOfOperation, - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.FanModeSequence, -} - -local function test_init() - mock_device:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do +local function get_subscribe_request(device, attribute_list) + local subscribe_request = attribute_list[1]:subscribe(device) + for i, cluster in ipairs(attribute_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) + subscribe_request:merge(cluster:subscribe(device)) end end - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + return subscribe_request +end + +local function test_init() + test.disable_startup_messages() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -171,19 +159,14 @@ local function test_init() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, get_subscribe_request(mock_device, cluster_subscribe_list)}) end test.set_test_init_function(test_init) local function test_init_disorder_endpoints() - mock_device_disorder_endpoints:set_field("MIN_SETPOINT_DEADBAND_CHECKED", 1, {persist = true}) - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request_disorder_endpoints = cluster_subscribe_list_disorder_endpoints[1]:subscribe(mock_device_disorder_endpoints) - for i, cluster in ipairs(cluster_subscribe_list_disorder_endpoints) do - if i > 1 then - subscribe_request_disorder_endpoints:merge(cluster:subscribe(mock_device_disorder_endpoints)) - end - end - test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, subscribe_request_disorder_endpoints}) + test.disable_startup_messages() test.mock_device.add_test_device(mock_device_disorder_endpoints) test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "added" }) @@ -194,25 +177,79 @@ local function test_init_disorder_endpoints() read_req:merge(clusters.FanControl.attributes.RockSupport:read()) read_req:merge(clusters.Thermostat.attributes.AttributeList:read()) test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, read_req}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "init" }) + test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, get_subscribe_request( + mock_device_disorder_endpoints, cluster_subscribe_list)}) end +-- run the profile configuration tests +local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) + test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) + generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + + test.wait_for_events() + + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) + device_info_copy.profile.id = "thermostat-modular" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) +end + +local expected_metadata = { + optional_component_capabilities={ + { + "main", + { + "relativeHumidityMeasurement", + "fanMode", + "fanOscillationMode", + "thermostatHeatingSetpoint", + "thermostatCoolingSetpoint" + }, + }, + }, + profile="thermostat-modular", +} + +local new_cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.RockSupport, -- These two attributes will be subscribed to following the profile + clusters.FanControl.attributes.RockSetting, -- change since the fanOscillationMode capability will be enabled. +} + test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support", function() - mock_device:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) - mock_device:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device, expected_metadata, + get_subscribe_request(mock_device, new_cluster_subscribe_list)) end ) test.register_coroutine_test( "Profile change on doConfigure lifecycle event no battery & state support with disorder endpoints", function() - mock_device_disorder_endpoints:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) - test.socket.device_lifecycle:__queue_receive({mock_device_disorder_endpoints.id, "doConfigure"}) - mock_device_disorder_endpoints:expect_metadata_update({ profile = "thermostat-humidity-fan-nostate-nobattery" }) - mock_device_disorder_endpoints:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test_thermostat_device_type_update_modular_profile(mock_device_disorder_endpoints, expected_metadata, + get_subscribe_request(mock_device_disorder_endpoints, new_cluster_subscribe_list)) end, { test_init = test_init_disorder_endpoints } ) 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 767b21a36e..44ed395797 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local clusters = require "st.matter.clusters" @@ -219,6 +218,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -238,6 +242,11 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 1, 40*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { maximum = 100.0, minimum = 0.0, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -703,28 +712,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { 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 d8146257ef..e901ac59a4 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 @@ -15,8 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" - local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -157,6 +155,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -177,6 +181,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedCoolingSetpoint:build_test_report_data(mock_device, 3, 40 * 100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -513,29 +523,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 64 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 3, - utils.round((64 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Setting the mode to cool should send the appropriate commands", { 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 5e44701d97..9c42c03ddb 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 @@ -14,12 +14,13 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" - local clusters = require "st.matter.clusters" +local dkjson = require "dkjson" +local im = require "st.matter.interaction_model" +local uint32 = require "st.matter.data_types.Uint32" +local utils = require "st.utils" -test.set_rpc_version(8) +test.disable_startup_messages() local mock_device_basic = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -49,10 +50,10 @@ local mock_device_basic = test.mock_device.build_test_matter_device({ }, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0}, }, device_types = { - {device_type_id = 0x0301, device_type_revision = 1}, -- Thermostat + {device_type_id = 0x0301, device_type_revision = 1} -- Thermostat } } } @@ -67,12 +68,12 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) return subscribe_request end local subscribe_request_basic local function test_init() + test.mock_device.add_test_device(mock_device_basic) local subscribed_attributes = { clusters.Thermostat.attributes.LocalTemperature, clusters.Thermostat.attributes.OccupiedCoolingSetpoint, @@ -92,14 +93,35 @@ local function test_init() clusters.FanControl.attributes.FanModeSequence, clusters.PowerSource.attributes.BatPercentRemaining, } + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "added" }) + local read_attributes = { + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.RockSupport, + clusters.Thermostat.attributes.AttributeList, + } + local read_request = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + for _, clus in ipairs(read_attributes) do + read_request:merge(clus:read(mock_device_basic)) + end + test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) end -- run the profile configuration tests local function test_thermostat_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) - generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + generic_mock_device.id, + clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {uint32(0)}) + }) + generic_mock_device:expect_metadata_update(expected_metadata) + local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) device_info_copy.profile.id = "thermostat-modular" local device_info_json = dkjson.encode(device_info_copy) @@ -125,8 +147,6 @@ local expected_metadata = { test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities", function() - mock_device_basic:set_field("__BATTERY_SUPPORT", "NO_BATTERY") -- since we're assuming this would have happened during device_added in this case. - mock_device_basic:set_field("__THERMOSTAT_RUNNING_STATE_SUPPORT", false) -- since we're assuming this would have happened during device_added in this case. test_thermostat_device_type_update_modular_profile(mock_device_basic, expected_metadata, subscribe_request_basic) end, { test_init = test_init } 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 new file mode 100644 index 0000000000..b52b08027e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua @@ -0,0 +1,145 @@ +-- Copyright 2022 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 clusters = require "st.matter.clusters" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" + +-- Temperature values are converted to Celsius by the hub before reaching the driver for rpc > 5. +-- This test file is meant to verify that the driver converts Fahrenheit values > 40 degrees from F to C for rpc <= 5. +test.set_rpc_version(5) + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + 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.FanControl.ID, cluster_type = "SERVER"}, + { + cluster_id = clusters.Thermostat.ID, + cluster_revision=5, + cluster_type="SERVER", + feature_map=3, -- Heat and Cool features + }, + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + } + } + } +}) + +local function test_init() + local cluster_subscribe_list = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ThermostatRunningState, + clusters.Thermostat.attributes.ControlSequenceOfOperation, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.FanModeSequence, + clusters.PowerSource.attributes.BatPercentRemaining, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + 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_message_test( + "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((90 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 41 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, + utils.round((41 - 32) * (5 / 9.0) * 100)) + } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 35 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1, 35 * 100) + } + } + } +) + +test.run_registered_tests() 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 ee8365c30c..0dbeff9796 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 @@ -15,7 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" local version = require "version" local clusters = require "st.matter.clusters" @@ -106,29 +105,6 @@ local function test_init() end test.set_test_init_function(test_init) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 90 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, - utils.round((90 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Heating setpoint reports should generate correct messages", { @@ -140,6 +116,12 @@ test.register_message_test( clusters.Thermostat.server.attributes.OccupiedHeatingSetpoint:build_test_report_data(mock_device, 1, 70*100) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 0.00, maximum = 100.00, step = 0.1 }, unit = "C" })) + }, { channel = "capability", direction = "send", @@ -170,28 +152,6 @@ test.register_message_test( } ) -test.register_message_test( - "Setting the heating setpoint to a Fahrenheit value should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_device.id, - { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 100 } } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, WATER_HEATER_EP, utils.round((100 - 32) * (5 / 9.0) * 100)) - } - } - } -) - test.register_message_test( "Ensure WaterHeaderMode supportedModes are registered and setting Oven mode should send appropriate commands", { 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 2d7e3946ef..4c1eab7443 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 @@ -17,8 +17,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" + local WindowCovering = clusters.WindowCovering +test.disable_startup_messages() + local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), @@ -36,44 +39,12 @@ local mock_device = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters - { - cluster_id = clusters.WindowCovering.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 3, - }, - {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} - }, - }, - }, - } -) - -local mock_device_switch_to_battery = test.mock_device.build_test_matter_device( - { - profile = t_utils.get_profile_definition("window-covering.yml"), - manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, - endpoints = { - { - endpoint_id = 2, clusters = { - {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, - }, - device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode - } - }, - { - endpoint_id = 10, - clusters = { -- list the clusters { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", cluster_revision = 1, - feature_map = 1, + feature_map = 3, }, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = 0x0002} @@ -100,7 +71,7 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( }, { endpoint_id = 10, - clusters = { -- list the clusters + clusters = { { cluster_id = clusters.WindowCovering.ID, cluster_type = "SERVER", @@ -130,34 +101,45 @@ local CLUSTER_SUBSCRIBE_LIST_NO_BATTERY = { } local function test_init() + 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.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) 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.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) -end -local function test_init_switch_to_battery() - local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_switch_to_battery) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_switch_to_battery)) end - end - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_switch_to_battery) - test.socket.device_lifecycle:__queue_receive({ mock_device_switch_to_battery.id, "doConfigure" }) - mock_device_switch_to_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device_switch_to_battery.id, read_attribute_list}) + test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end local function test_init_mains_powered() + test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "added" }) + test.socket.capability:__expect_send( + mock_device_mains_powered:generate_test_message( + "main", capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, + {visibility = {displayed = false}}) + ) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "init" }) local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_mains_powered) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_mains_powered)) end end test.socket.matter:__expect_send({mock_device_mains_powered.id, subscribe_request}) - test.mock_device.add_test_device(mock_device_mains_powered) + test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "doConfigure" }) mock_device_mains_powered:expect_metadata_update({ profile = "window-covering" }) mock_device_mains_powered:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -794,13 +776,12 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(12)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(12)}) } ) - mock_device_switch_to_battery:expect_metadata_update({ profile = "window-covering-battery" }) - end, - { test_init = test_init_switch_to_battery } + mock_device:expect_metadata_update({ profile = "window-covering-tilt-battery" }) + end ) test.register_coroutine_test( @@ -808,12 +789,11 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - mock_device_switch_to_battery.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_switch_to_battery, 10, {uint32(10)}) + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(10)}) } ) - end, - { test_init = test_init_switch_to_battery } + end ) test.register_coroutine_test( 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 19673a958a..d9671a5283 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 @@ -199,6 +199,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 b01e88e62b..d7076ec9ee 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua @@ -147,6 +147,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 79e5c68fa5..e288d3a927 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 @@ -80,6 +80,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -97,6 +105,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index cf136ae1fd..31dc36ce7b 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -90,6 +90,6 @@ local zigbee_humidity_driver = { health_check = false, } -defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities) +defaults.register_for_default_handlers(zigbee_humidity_driver, zigbee_humidity_driver.supported_capabilities, {native_capability_attrs_enabled = true}) local driver = ZigbeeDriver("zigbee-humidity-sensor", zigbee_humidity_driver) driver:run() 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 104c36c3e5..8f414975a1 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 @@ -167,6 +167,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -186,6 +194,7 @@ test.register_coroutine_test( ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 9334c03045..d9745bb681 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 @@ -116,6 +116,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -156,6 +164,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 ca121baa9b..f3e7831b1f 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,6 +48,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -151,6 +159,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { 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 f3d30ab0c8..9afd1c3a1d 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,6 +60,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -171,6 +179,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() test.socket.zigbee:__queue_receive( { 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 7da190b523..d4f3c09a05 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,6 +55,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 003c7d95bf..b016503f44 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 @@ -143,6 +143,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -407,6 +415,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" }))) + mock_device:expect_native_attr_handler_registration("temperatureMeasurement", "temperature") test.wait_for_events() end ) 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 753850e38e..cda87474fa 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 @@ -156,6 +156,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 afe97fb0f9..710469054f 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 @@ -99,7 +99,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -113,7 +113,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua index dcbb1b0b00..700eab4300 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua @@ -145,7 +145,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, 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 b8a685bd9a..ff4fa07184 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 @@ -270,6 +270,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 83717569f0..4ab2f31e92 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 @@ -267,6 +267,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 0301c0228c..319066fa3b 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 @@ -202,6 +202,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -340,7 +348,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -354,7 +362,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -552,6 +560,7 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:build_test_attr_report(mock_device, 556)}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + mock_device:expect_native_attr_handler_registration("colorTemperature", "colorTemperature") end ) 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 8fb1704f38..8b9b26ba13 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua @@ -222,6 +222,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2.7, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 4605e8bf8b..c9e16aadad 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 @@ -329,6 +329,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -348,6 +356,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_device:generate_test_message("main", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 3e3eb5722a..c1be129215 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 @@ -105,6 +105,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 34cbff59fa..0c66abd359 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 @@ -105,6 +105,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.0, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 4115546f55..74598ef3a5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -72,7 +72,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 1, 3600, 5) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) } ) test.socket.zigbee:__expect_send( @@ -90,7 +90,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 5) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) } ) 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 28460999f2..88470b5489 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 @@ -118,6 +118,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 9.766, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 bdb2b681cb..01f1a0ba74 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua @@ -120,7 +120,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) - } + }, } ) 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 fd6403d1cd..4ee674199c 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 @@ -247,6 +247,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 1d3480ef99..13bdb14268 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 @@ -253,6 +253,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 154413af45..43543e3127 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 @@ -215,6 +215,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7b85749b29..bc476f06bb 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 @@ -135,6 +135,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 8896cbba36..e4ffe5567c 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 @@ -213,6 +213,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 8ad7bef789..ab0163ef5c 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 @@ -141,6 +141,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index a407322212..144be985ae 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -22,7 +22,6 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 --- @type st.zwave.CommandClass.SensorBinary local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) --- @type st.zwave.CommandClass.SensorMultilevel -local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) local preferences = require "preferences" local configurations = require "configurations" @@ -73,14 +72,6 @@ local function sensor_binary_report_handler(self, device, cmd) device:emit_event(event) end -local function sensor_multilevel_report_handler(self, device, cmd) - if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then - local scale = 'C' - if (cmd.args.scale == SensorMultilevel.scale.temperature.FAHRENHEIT) then scale = 'F' end - device:emit_event(capabilities.temperatureMeasurement.temperature({value = cmd.args.sensor_value, unit = scale})) - end -end - local function do_configure(driver, device) configurations.initial_configuration(driver, device) -- The flood sensor can be hardwired, so update any preferences @@ -101,9 +92,6 @@ local fibaro_flood_sensor = { [cc.SENSOR_BINARY] = { [SensorBinary.REPORT] = sensor_binary_report_handler }, - [cc.SENSOR_MULTILEVEL] = { - [SensorMultilevel.REPORT] = sensor_multilevel_report_handler - } }, lifecycle_handlers = { doConfigure = do_configure 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 4cd8d0e905..fbea573d09 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 @@ -344,6 +344,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -363,6 +371,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor1.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 b100af6f1a..2b27d66868 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 @@ -232,6 +232,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -251,6 +259,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_fibaro_door_window_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 909beb6b1d..7a9743a69e 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 @@ -130,6 +130,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_sensor:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25, unit = 'C'})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_sensor.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 25a7b6ee83..24a7f31eaf 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 @@ -312,6 +312,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -331,6 +339,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70.7, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 aa7d1ff60c..643c69dd1f 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -173,7 +173,7 @@ test.register_message_test( ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (C) should be handled as temperature", { { channel = "zwave", @@ -188,12 +188,20 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 30, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) test.register_message_test( - "SensorMultilevel report temperature should be handled as temperature", + "SensorMultilevel report temperature (F) should be handled as temperature", { { channel = "zwave", @@ -208,6 +216,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 70, unit = "F" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -266,6 +282,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -286,6 +310,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 50, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 e97c97029b..2321b5bd09 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 @@ -200,6 +200,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) @@ -220,6 +228,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 37, unit = 'F' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 f172ba9fde..07e64cffa2 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 @@ -296,6 +296,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 11f4707f8a..b10fdfe4c9 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 @@ -320,6 +320,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 11746559a5..42144a996a 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 @@ -214,6 +214,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -387,6 +395,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_child.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 a49bd678d9..4c7d4c8583 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -2046,6 +2046,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_5:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 20, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 e38df9c0f5..bd846c93b2 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 @@ -323,6 +323,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 49f1117d41..8481055813 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 @@ -335,6 +335,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -359,6 +367,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_child_2_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 d9588faa15..77663604d5 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 @@ -314,6 +314,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 3e3aca24f2..6f4a098ef7 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 @@ -163,6 +163,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 9f7f7d5b86..be1b64b05b 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 @@ -493,6 +493,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) @@ -514,6 +522,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_parent:generate_test_message("main", capabilities.powerMeter.power({ value = 89, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 5f7217472e..35f8b341dd 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 @@ -234,6 +234,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 55, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 abfaa48ffd..13d3e90a3c 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,6 +57,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 8b526a9984..2a96961755 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 @@ -64,6 +64,14 @@ test.register_message_test( meter_value = 27}) )} }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } } ) 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 56ffdd939d..1905fec5f6 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,6 +68,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_switch:generate_test_message("main", capabilities.powerMeter.power({ value = 27, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_switch.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 6393613025..69034f9b5a 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua @@ -146,6 +146,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7cbc6e38e5..cac8b883e4 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 @@ -244,6 +244,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 7ff628f974..3671cd4029 100755 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua @@ -95,8 +95,16 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } } } + } ) test.register_message_test( 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 00056c753a..2c9b04d82d 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 @@ -471,6 +471,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 5, unit = "W" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } } } ) 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 b332713319..ea14555a0a 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua @@ -256,6 +256,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } } } ) 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 9c800e7070..9c879806f6 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 @@ -303,7 +303,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_fibaro_roller_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( 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 ce5ed9fb81..5431ed1e70 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 @@ -233,7 +233,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_qubino_flush_shutter.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index bdc41956ec..f1a841c626 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -214,7 +214,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -251,7 +251,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -374,7 +374,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( @@ -415,7 +415,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_blind_v3.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.capability:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua index 4335ae3b41..0b2d209f2a 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_springs_window_fashion_shade.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index bf60c1797b..bc3a087093 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -304,7 +304,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive( { mock_window_shade_switch_multilevel.id, - { capability = "windowShadePreset", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) test.socket.zwave:__expect_send( From 910e30eaf09fc4381a937ab482da1bb8c6984a36 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Fri, 22 Aug 2025 08:09:19 -0500 Subject: [PATCH 077/449] matter-switch: Add profile for 9-button device https://smartthings.atlassian.net/browse/CHAD-16315 --- .../profiles/9-button-battery.yml | 63 +++++++++++++++++++ .../profiles/9-button-batteryLevel.yml | 62 ++++++++++++++++++ .../matter-switch/profiles/9-button.yml | 60 ++++++++++++++++++ .../SmartThings/matter-switch/src/init.lua | 2 +- 4 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button-battery.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/9-button.yml diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml new file mode 100644 index 0000000000..8aa7df8e39 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-battery.yml @@ -0,0 +1,63 @@ +name: 9-button-battery +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + diff --git a/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml new file mode 100644 index 0000000000..ae90de2d01 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button-batteryLevel.yml @@ -0,0 +1,62 @@ +name: 9-button-batteryLevel +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/9-button.yml b/drivers/SmartThings/matter-switch/profiles/9-button.yml new file mode 100644 index 0000000000..3894a40d13 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/9-button.yml @@ -0,0 +1,60 @@ +name: 9-button +components: + - id: main + capabilities: + - id: button + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 57335bf10b..a22904e517 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -289,7 +289,7 @@ local START_BUTTON_PRESS = "__start_button_press" local TIMEOUT_THRESHOLD = 10 --arbitrary timeout local HELD_THRESHOLD = 1 -- this is the number of buttons for which we have a static profile already made -local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8} +local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8, 9} -- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a -- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because From 61212921fdef3a2dce547105148bce7f54b4ee46 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 25 Aug 2025 13:07:54 -0700 Subject: [PATCH 078/449] WWSTCERT-7616 Hue W 1600 A21 E26 1P NAM (#2354) * WWSTCERT-7616 Hue W 1600 A21 E26 1P NAM * WWSTCERT-7610 Hue WA 810 A19 E26 1P NAM WWSTCERT-7613 Hue WCA 650 BR30 E26 1P NAM * fixup --- .../SmartThings/matter-switch/fingerprints.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b6b2540755..13511bb728 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -439,6 +439,23 @@ matterManufacturer: productId: 0x6008 deviceProfileName: light-color-level-2700K-6500K +# Hue + - id: "4107/2049" + deviceLabel: Hue W 1600 A21 E26 1P NAM + vendorId: 0x100B + productId: 0x0801 + deviceProfileName: light-level + - id: "4107/297" + deviceLabel: Hue WCA 650 BR30 E26 1P NAM + vendorId: 0x100B + productId: 0x0129 + deviceProfileName: light-color-level + - id: "4107/2048" + deviceLabel: Hue WA 810 A19 E26 1P NAM + vendorId: 0x100B + productId: 0x0800 + deviceProfileName: light-level-colorTemperature + #Ledvance - id: "4489/843" deviceLabel: Matter Filament RGBW From 13ad1a0e813093de5a02edc4a2ae23e407fdce84 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 25 Aug 2025 13:45:31 -0700 Subject: [PATCH 079/449] WWSTCERT-7387 Intecular InvisOutlet (#2318) * WWSTCERT-7387 Intecular InvisOutlet * update profile --- 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 13511bb728..aa95b8d4dc 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -455,6 +455,12 @@ matterManufacturer: vendorId: 0x100B productId: 0x0800 deviceProfileName: light-level-colorTemperature +# Intecular + - id: "5226/32769" + deviceLabel: InvisOutlet + vendorId: 0x146A + productId: 0x8001 + deviceProfileName: plug-binary #Ledvance - id: "4489/843" From a8b796a92d733f19b5dbd75919e2c270acbb95e7 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 25 Aug 2025 15:53:06 -0500 Subject: [PATCH 080/449] CHAD-15951: fix moved matter powerMeter NH registration to not rely on endpoint id --- drivers/SmartThings/matter-switch/src/init.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 57335bf10b..cd388b4b6e 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1206,13 +1206,13 @@ local function active_power_handler(driver, device, ib, response) local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT if ib.endpoint_id ~= 0 then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end else -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end end end From d7d54ddb6fd9bab919e07cabae8d37e871a46aee Mon Sep 17 00:00:00 2001 From: Jeron Aldaron Lau Date: Tue, 13 May 2025 17:33:11 -0500 Subject: [PATCH 081/449] Fix broken link for contributing code guidelines --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3eb6fb07cc..c87d2c66c2 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ By submitting a pull request, you represent that you have the right to license your contribution to SmartThings and agree by submitting your patch that your contributions are licensed under the [Apache 2.0 license](LICENSE). Before submitting your pull request, please make sure you have tested your changes and that -they follow the project guidelines for [contributing code](https://developer.smartthings.com/docs/devices/hub-connected/certify-your-device#code-formatting-and-submission-criteria). +they follow the project guidelines for [contributing code](https://developer.smartthings.com/docs/devices/hub-connected/code-formatting-criteria). Before contributions can be merged, all contributors must agree to the [SmartThings Individual Contributor License From edf8f68f83ad4be440df5f39d079f9875323894a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 10 Jul 2025 15:42:55 -0700 Subject: [PATCH 082/449] WWSTCERT-6985 Sensereo Matter Smoke Alarm MS1 --- drivers/SmartThings/matter-sensor/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3753e8b23b..a3c6643c02 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -158,6 +158,12 @@ matterManufacturer: vendorId: 0x102E productId: 0x2030 deviceProfileName: motion-illuminance-battery + # Sensereo + - id: "5526/1" + deviceLabel: Sensereo Matter Smoke Alarm MS1 + vendorId: 0x1596 + productId: 0x0001 + deviceProfileName: smoke-battery # Siterwell - id: "4736/847" deviceLabel: Siterwell Door Window Sensor From eeaf95e088f50d815e67d58775aa1f32ffd62b05 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 30 Jun 2025 16:36:47 -0700 Subject: [PATCH 083/449] WWSTCERT-6685 Yale Lock with Matter --- 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 36b7d115f1..441e3ae8ab 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -104,6 +104,12 @@ matterManufacturer: vendorId: 0x147F productId: 0x0008 deviceProfileName: lock-user-pin-battery + #Yale + - id: "4125/33040" + deviceLabel: Yale Lock with Matter + vendorId: 0x101D + productId: 0x8110 + deviceProfileName: lock-user-pin-schedule-battery matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock From 1acd1dc4576bea5c70339b224944c28263c42b0b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 28 Aug 2025 08:27:29 -0700 Subject: [PATCH 084/449] WWSTCERT-7683 First Alert SMCO410 --- drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml index 6a73e78428..39b541becb 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml +++ b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml @@ -63,3 +63,9 @@ zwaveManufacturer: productType: 0x0004 productId: 0x0003 deviceProfileName: co-battery + - id: "1051/1/1040" + deviceLabel: SMCO410 + manufacturerId: 0x041B + productId: 0x0410 + productType: 0x0001 + deviceProfileName: smoke-co-battery From d6c703e250bf25c16bc06bab1acbbdd9fe1e1430 Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Fri, 22 Aug 2025 14:27:57 +0800 Subject: [PATCH 085/449] This commit fixes the issue of some devices "false alarm" after hub switch-over. We add check_latest state before sending initial event(open/unlocked/presense) for following drivers: - SmartThings/zigbee-contact/src/aqara/ - SmartThings/zigbee-lock/src/samsungsds - SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1 - SmartThings/zigbee-presence-sensor/src (top-most driver) - SmartThings/zigbee-thermostat/src/aqara - SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1 - SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2 --- .../zigbee-contact/src/aqara/init.lua | 8 +- .../src/test/test_aqara_contact_sensor.lua | 10 +- .../zigbee-lock/src/samsungsds/init.lua | 8 +- .../src/test/test_zigbee_samsungsds.lua | 6 + .../src/arrival-sensor-v1/init.lua | 8 +- .../zigbee-presence-sensor/src/init.lua | 8 +- .../src/test/test_st_arrival_sensor_v1.lua | 8 ++ .../src/test/test_zigbee_presence_sensor.lua | 12 ++ .../zigbee-thermostat/src/aqara/init.lua | 10 +- .../src/test/test_aqara_thermostat.lua | 114 ++++++++++++------ .../fibaro-door-window-sensor-1/init.lua | 11 +- .../fibaro-door-window-sensor-2/init.lua | 12 +- .../test/test_fibaro_door_window_sensor_1.lua | 31 +++++ .../test/test_fibaro_door_window_sensor_2.lua | 7 ++ 14 files changed, 200 insertions(+), 53 deletions(-) diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua index 46da673401..a952e0bfef 100644 --- a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua @@ -63,11 +63,17 @@ local function do_configure(self, device) PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(driver, device) device:emit_event(capabilities.batteryLevel.type("CR1632")) device:emit_event(capabilities.batteryLevel.quantity(1)) device:emit_event(capabilities.batteryLevel.battery("normal")) - device:emit_event(capabilities.contactSensor.contact.closed()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) end local function contact_status_handler(self, device, value, zb_rx) 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 0fd017d6b0..119e3fbdab 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 @@ -81,13 +81,21 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle handler", function() + -- The initial contactSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.type("CR1632"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.quantity(1))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.contactSensor.contact.closed())) + capabilities.contactSensor.contact.open())) + test.wait_for_events() + -- Avoid sending the initial contactSensor 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.batteryLevel.type("CR1632"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.quantity(1))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) end ) diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index c0a597a99d..b529dd3fd1 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -57,9 +57,15 @@ local refresh = function(driver, device, cmd) -- do nothing in refresh capability handler end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local device_added = function(self, device) lock_utils.populate_state_from_data(device) - device:emit_event(capabilities.lock.lock.unlocked()) + 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 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 1667b0ecb8..bac1554790 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua @@ -1377,11 +1377,17 @@ test.register_coroutine_test( test.register_coroutine_test( "Device added function handler", function() + -- The initial lock event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) test.wait_for_events() + -- Avoid sending the initial lock 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:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) + test.wait_for_events() end ) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua index dd0566a3a0..b7bece2efe 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua @@ -100,8 +100,14 @@ local function beep_handler(self, device, command) end end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(self, device) - device:emit_event(PresenceSensor.presence("present")) + emit_event_if_latest_state_missing(device, "main", PresenceSensor, PresenceSensor.presence.NAME, PresenceSensor.presence("present")) end local function init_handler(self, device, event, args) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index 1ab62e821d..d52722ed16 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -139,8 +139,14 @@ local function beep_handler(self, device, command) device:send(IdentifyCluster.server.commands.Identify(device, BEEP_IDENTIFY_TIME)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function added_handler(self, device) - device:emit_event(PresenceSensor.presence("present")) + emit_event_if_latest_state_missing(device, "main", PresenceSensor, PresenceSensor.presence.NAME, PresenceSensor.presence("present")) device:set_field(IS_PRESENCE_BASED_ON_BATTERY_REPORTS, false, {persist = true}) device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) end 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 0ddffcedab..4c2533036d 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 @@ -68,6 +68,7 @@ end zigbee_test_utils.prepare_zigbee_env_info() local add_device = function() + -- The initial presenceSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present") @@ -75,6 +76,12 @@ local add_device = function() test.wait_for_events() end +local add_device_after_switch_over = function() + -- Avoid sending the initial presenceSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) + test.wait_for_events() +end + local function test_init() test.mock_device.add_test_device(mock_simple_device)end @@ -126,6 +133,7 @@ test.register_coroutine_test( "Added lifecycle should be handlded", function () add_device() + add_device_after_switch_over() end ) 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 30fec61f6d..55a0d7cf51 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 @@ -109,6 +109,7 @@ test.register_message_test( ) local add_device = function() + -- The initial presenceSensor event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present") @@ -120,6 +121,16 @@ local add_device = function() test.wait_for_events() end +local add_device_after_switch_over = function() + -- Avoid sending the initial presenceSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added"}) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_simple_device) + }) + test.wait_for_events() +end + test.register_coroutine_test( "Battery Voltage test cases when polling from hub", function() @@ -185,6 +196,7 @@ test.register_coroutine_test( "Added lifecycle should be handlded", function () add_device() + add_device_after_switch_over() end ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua index 9523614bcc..662d337b82 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua @@ -110,17 +110,23 @@ local function device_init(driver, device) do_refresh(driver, device) end +local function emit_component_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component.id, capability.ID, attribute_name) == nil then + device:emit_component_event(component, value) + end +end + local function device_added(driver, device) supported_thermostat_modes_handler(driver, device, nil) device:emit_event(capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) device:emit_event(capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) device:emit_event(capabilities.thermostatMode.thermostatMode.manual()) - device:emit_event(capabilities.valve.valve.open()) - device:emit_component_event(device.profile.components.ChildLock, capabilities.lock.lock.unlocked()) device:emit_event(capabilities.hardwareFault.hardwareFault.clear()) device:emit_event(valveCalibration.calibrationState.calibrationPending()) device:emit_event(invisibleCapabilities.invisibleCapabilities({""})) device:emit_event(capabilities.battery.battery(100)) + emit_component_event_if_latest_state_missing(device, device.profile.components.main, capabilities.valve, capabilities.valve.valve.NAME, capabilities.valve.valve.open()) + emit_component_event_if_latest_state_missing(device, device.profile.components.ChildLock, capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) end local function thermostat_alarm_status_handler(driver, device, value, zb_rx) 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 355c48b240..4a1dc2057f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -73,46 +73,8 @@ end test.set_test_init_function(test_init) --- test.register_coroutine_test( --- "Handle added lifecycle", --- function() --- test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", --- capabilities.thermostatMode.supportedThermostatModes({ --- capabilities.thermostatMode.thermostatMode.manual.NAME, --- capabilities.thermostatMode.thermostatMode.antifreezing.NAME --- }, { visibility = { displayed = false } })) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.valve.valve.open()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) --- ) --- test.socket.capability:__expect_send( --- mock_device:generate_test_message("main", capabilities.battery.battery(100)) --- ) --- end --- ) + + test.register_coroutine_test( @@ -312,4 +274,76 @@ test.register_coroutine_test( end ) --]] +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial valve and lock event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.manual.NAME, + capabilities.thermostatMode.thermostatMode.antifreezing.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.valve.valve.open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked()) + ) + -- Avoid sending the initial open and lock 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.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.manual.NAME, + capabilities.thermostatMode.thermostatMode.antifreezing.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 27.0, unit = "C"})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.manual()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationPending()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua index 63f4d8d8a0..698fffcceb 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua @@ -66,13 +66,18 @@ local function do_configure(driver, device) device:send(Association:Remove({grouping_identifier = 1, node_ids = driver.environment_info.hub_zwave_id})) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(driver, device) do_refresh(driver, device) - device:emit_event(capabilities.tamperAlert.tamper.clear()) - device:emit_event(capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.tamperAlert, capabilities.tamperAlert.tamper.NAME, capabilities.tamperAlert.tamper.clear()) end - local fibaro_door_window_sensor_1 = { NAME = "fibaro door window sensor 1", lifecycle_handlers = { diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua index 6290f15a41..16c5ec2017 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua @@ -33,10 +33,16 @@ local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, return false end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(self, device) - device:emit_event(capabilities.tamperAlert.tamper.clear()) - device:emit_event(capabilities.contactSensor.contact.open()) - device:emit_event(capabilities.temperatureAlarm.temperatureAlarm.cleared()) + emit_event_if_latest_state_missing(device, "main", capabilities.tamperAlert, capabilities.tamperAlert.tamper.NAME, capabilities.tamperAlert.tamper.clear()) + emit_event_if_latest_state_missing(device, "main", capabilities.contactSensor, capabilities.contactSensor.contact.NAME, capabilities.contactSensor.contact.open()) + emit_event_if_latest_state_missing(device, "main", capabilities.temperatureAlarm, capabilities.temperatureAlarm.temperatureAlarm.NAME, capabilities.temperatureAlarm.temperatureAlarm.cleared()) end local function alarm_report_handler(self, device, cmd) 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 4cd8d0e905..7975e8f9b1 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 @@ -58,6 +58,7 @@ test.set_test_init_function(test_init) test.register_message_test( "Device should be polled with refresh right after inclusion", { + -- The initial tamperAlert and contactSensor event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -96,6 +97,36 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor1:generate_test_message("main", capabilities.contactSensor.contact.open()) + }, + -- Avoid sending the initial tamperAlert and contactSensor event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_fibaro_door_window_sensor1.id, "added" } + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + Battery:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + SensorBinary:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_fibaro_door_window_sensor1, + SensorAlarm:Get({}) + ) } }, { 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 fccd88ad97..bc78bd02ca 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 @@ -339,6 +339,7 @@ test.register_coroutine_test( test.register_message_test( "device_added should be handled", { + -- The initial tamperAlert, contactSensor & temperatureAlarm event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -358,6 +359,12 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_fibaro_door_window_sensor:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + }, + -- Avoid sending the initial tamperAlert, contactSensor & temperatureAlarm event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_fibaro_door_window_sensor.id, "added"} } } ) From 9f0a2f5de7fced75c4cd785973197da2a8459db2 Mon Sep 17 00:00:00 2001 From: RarbourLeviton Date: Tue, 2 Sep 2025 15:20:14 -0400 Subject: [PATCH 086/449] Update leviton-zw6hd.yml to expand range 99->100 We are seeing issues when the zwave range is report is 99, the ST app claims it is out of range. All other drivers go to 100 range, so I believe this is the source of the issue, so changing our driver to match --- drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml b/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml index 53aa313718..3791a4422e 100644 --- a/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml +++ b/drivers/SmartThings/zwave-switch/profiles/leviton-zw6hd.yml @@ -10,7 +10,7 @@ components: config: values: - key: "level.value" - range: [1, 99] + range: [1, 100] - id: refresh version: 1 categories: From bf8461d8696115223492b2525050da51ae107bbb Mon Sep 17 00:00:00 2001 From: GAFfrient <96058156+GAFfrient@users.noreply.github.com> Date: Wed, 3 Sep 2025 20:03:07 +0200 Subject: [PATCH 087/449] Merge pull request #2358 from GAFfrient/frient-add-support-for-SBTZB-110 Add support for SBTZB-110 --- .../zigbee-button/fingerprints.yml | 7 +- .../profiles/button-profile-frient.yml | 46 +++ .../profiles/button-profile-panic-frient.yml | 85 ++++++ .../zigbee-button/src/frient/init.lua | 191 ++++++++++-- .../SmartThings/zigbee-button/src/init.lua | 3 +- .../src/test/test_frient_button.lua | 286 ++++++++++++++---- 6 files changed, 545 insertions(+), 73 deletions(-) create mode 100644 drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml create mode 100644 drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 422901141f..7acc77387f 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -23,7 +23,12 @@ zigbeeManufacturer: deviceLabel: frient Button manufacturer: frient A/S model: MBTZB-110 - deviceProfileName: button-profile + deviceProfileName: button-profile-frient + - id: "frient A/S/SBTZB-110" + deviceLabel: frient Button + manufacturer: frient A/S + model: SBTZB-110 + deviceProfileName: button-profile-frient - id: "LDS/ZBT-CCTSwitch-D0001" deviceLabel: EcoSmart Remote Control manufacturer: LDS diff --git a/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml b/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml new file mode 100644 index 0000000000..c817709b4d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/button-profile-frient.yml @@ -0,0 +1,46 @@ +name: button-profile-frient +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController +preferences: + - title: "LED Color" + name: ledColor + description: "Color of LED when button is pressed (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Off" + 1: "Red" + 2: "Green" + 3: "Yellow" + default: "2" + - title: "Button press delay (ms)" + name: buttonDelay + description: "Delay before button press is registered in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 100 + - title: "Use as Panic Button" + name: panicButton + description: "Use as Panic Button (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting. May take up to 1 minute to reconfigure the device)." + required: false + preferenceType: enumeration + definition: + options: + 0xFFFF: "Off" + 0x002C: "On" + default: "0xFFFF" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml b/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml new file mode 100644 index 0000000000..61bb0d0add --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/button-profile-panic-frient.yml @@ -0,0 +1,85 @@ +name: button-profile-panic-frient +components: + - id: main + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: panicAlarm + version: 1 + categories: + - name: RemoteController +preferences: + - title: "LED Color" + name: ledColor + description: "Color of LED when button is pressed (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Off" + 1: "Red" + 2: "Green" + 3: "Yellow" + default: "2" + - title: "Button press delay (ms)" + name: buttonDelay + description: "Delay before button press is registered in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 100 + - title: "Use as Panic Button" + name: panicButton + description: "Use as Panic Button (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting. May take up to 1 minute to reconfigure the device)." + required: false + preferenceType: enumeration + definition: + options: + 0xFFFF: "Off" + 0x002C: "On" + default: "0x002C" + - title: "Hold to activate alarm (ms)" + name: buttonAlarmDelay + description: "Delay before button press is registered as alarm in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 2000 + - title: "Hold to cancel alarm (ms)" + name: buttonCancelDelay + description: "Delay before button press cancels alarm in milliseconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 8000 + default: 2000 + - title: "Auto alarm cancel (s)" + name: autoCancel + description: "Set the time the alarm will be automatically canceled in seconds (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65536 + default: 10 + - title: "Alarm LED behavior" + name: alarmBehavior + description: "Choose the behavior of the LED when the alarm is triggered (since it is a battery powered device, it is recommended to press the button to wake it up right before or right after changing this setting)." + required: false + preferenceType: enumeration + definition: + options: + 0: "Immediate" + 1: "Wait for confirmation" + default: "0" diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index eeed6e36a2..fbe07eefac 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -13,11 +13,20 @@ -- limitations under the License. local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local data_types = require "st.zigbee.data_types" local BasicInput = zcl_clusters.BasicInput local PowerConfiguration = zcl_clusters.PowerConfiguration +local OnOff = zcl_clusters.OnOff +local panicAlarm = capabilities.panicAlarm +local IASZone = zcl_clusters.IASZone + +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local BUTTON_LED_COLOR = 0x8002 +local BUTTON_PRESS_DELAY = 0x8001 +local PANIC_BUTTON = 0x8000 local battery_table = { [2.90] = 100, @@ -34,18 +43,121 @@ local battery_table = { [0.00] = 0 } -local function present_value_attr_handler(driver, device, value, zb_rx) - local event - local additional_fields = { - state_change = true +local CONFIGURATIONS = { + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.PresentValue.ID, + minimum_interval = 0, + maximum_interval = 21600, + data_type = BasicInput.attributes.PresentValue.base_type, + reportable_change = 1, + endpoint = 0x20 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30, + maximum_interval = 21600, + reportable_change = 1, } +} + +local PREFERENCE_TABLES = { + ledColor = { + clusterId = OnOff.ID, + attributeId = BUTTON_LED_COLOR, + dataType = data_types.Enum8, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x0C + }, + buttonDelay = { + clusterId = OnOff.ID, + attributeId = BUTTON_PRESS_DELAY, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x0C + }, + panicButton = { + clusterId = BasicInput.ID, + attributeId = PANIC_BUTTON, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x20, + frame_ctrl = 0x04 + }, + buttonAlarmDelay = { + clusterId = IASZone.ID, + attributeId = 0x8002, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + buttonCancelDelay = { + clusterId = IASZone.ID, + attributeId = 0x8003, + dataType= data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + autoCancel = { + clusterId = IASZone.ID, + attributeId = 0x8004, + dataType = data_types.Uint16, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, + alarmBehavior = { + clusterId = IASZone.ID, + attributeId = 0x8005, + dataType = data_types.Enum8, + mfg_code = DEVELCO_MANUFACTURER_CODE, + endpoint = 0x23, + frame_ctrl = 0x04 + }, +} + +local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) + if device:supports_capability(panicAlarm) then + if zone_status:is_alarm2_set() then + device:emit_event(panicAlarm.panicAlarm.panic({state_change = true})) + else + device:emit_event(panicAlarm.panicAlarm.clear({state_change = true})) + end + end +end + +local function configure_ias_zone_settings(driver, device) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 2000):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8003, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 2000):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8004, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 10):to_endpoint(0x23)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, IASZone.ID, 0x8005, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 0):to_endpoint(0x23)) +end + +local function present_value_attr_handler(driver, device, value, zb_rx) if value.value == true then - event = capabilities.button.button.pushed(additional_fields) - device:emit_event(event) + device:emit_event(capabilities.button.button.pushed({state_change = true})) end end -local function init_handler(self, device, event, args) +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + local zone_status = zb_rx.body.zcl_body.zone_status + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function init_handler(self, device) + for _,attribute in ipairs(CONFIGURATIONS) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end battery_defaults.enable_battery_voltage_table(device, battery_table) end @@ -55,30 +167,73 @@ local function added_handler(self, device) device:emit_event(capabilities.button.button.pushed({state_change = false})) end -local configure_handler = function(self, device) - device:send(device_management.build_bind_request(device, BasicInput.ID, self.environment_info.hub_zigbee_eui)) - device:send(BasicInput.attributes.PresentValue:configure_reporting(device, 0, 600, 1)) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryVoltage:configure_reporting(device, 30, 21600, 1)) +local function do_configure(driver, device, event, args) + device:configure() + device:send(cluster_base.write_manufacturer_specific_attribute(device, OnOff.ID, BUTTON_LED_COLOR, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 2):to_endpoint(0x20)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, OnOff.ID, BUTTON_PRESS_DELAY, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 100):to_endpoint(0x20)) + device:send(cluster_base.write_manufacturer_specific_attribute(device, BasicInput.ID, PANIC_BUTTON, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 0xFFFF):to_endpoint(0x20)) +end + +local function info_changed(driver, device, event, args) + for name, info in pairs(PREFERENCE_TABLES) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + local payload = tonumber(input) + + if (name == "panicButton") then + if (input == "0x002C")then + device:try_update_metadata({profile = "button-profile-panic-frient"}) + device.thread:call_with_delay(5, function() + device:emit_event(panicAlarm.panicAlarm.clear({state_change = true})) + configure_ias_zone_settings(driver,device) + end) + else + device:try_update_metadata({profile = "button-profile-frient"}) + end + end + + if (payload ~= nil) then + local message = cluster_base.write_manufacturer_specific_attribute( + device, + info.clusterId, + info.attributeId, + info.mfg_code, + info.dataType, + payload + ) + message.address_header.dest_endpoint.value = info.endpoint + message.body.zcl_header.frame_ctrl.value = info.frame_ctrl + device:send(message) + end + end + end end local frient_button = { NAME = "Frient Button Handler", lifecycle_handlers = { added = added_handler, - doConfigure = configure_handler, - init = init_handler + doConfigure = do_configure, + init = init_handler, + infoChanged = info_changed }, zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, [BasicInput.ID] = { [BasicInput.attributes.PresentValue.ID] = present_value_attr_handler } } }, can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "MBTZB-110" + return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SBTZB-110" or device:get_model() == "MBTZB-110") end } - return frient_button diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index de5d3bf755..776eaa8b9e 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -120,7 +120,8 @@ local zigbee_button_driver_template = { supported_capabilities = { capabilities.button, capabilities.battery, - capabilities.temperatureMeasurement + capabilities.panicAlarm, + capabilities.temperatureMeasurement, }, zigbee_handlers = { attr = { 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 725e0fa737..ef20adc678 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -15,29 +15,72 @@ -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" -local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff + +local panicAlarm = capabilities.panicAlarm.panicAlarm local button_attr = capabilities.button.button -local t_utils = require "integration_test.utils" + + +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local data_types = require "st.zigbee.data_types" local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("button-profile.yml"), + profile = t_utils.get_profile_definition("button-profile-frient.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "frient A/S", + model = "SBTZB-110", + server_clusters = {OnOff.ID}, + }, + [0x20] = { + id = 0x20, + server_clusters = {BasicInput.ID, PowerConfiguration.ID}, + client_clusters = {OnOff.ID}, + + }, + } + } +) +local mock_device_panic = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("button-profile-panic-frient.yml"), zigbee_endpoints = { [1] = { id = 1, manufacturer = "frient A/S", - model = "MBTZB-110", - server_clusters = {0x0001,0x0019} + model = "SBTZB-110", + server_clusters = {OnOff.ID}, + }, + [0x20] = { + id = 0x20, + server_clusters = {BasicInput.ID, PowerConfiguration.ID}, + client_clusters = {OnOff.ID}, + + }, + [0x23] = { + id = 0x23, + server_clusters = {IASZone.ID} } } } ) 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) + test.mock_device.add_test_device(mock_device_panic) + zigbee_test_utils.init_noop_health_check_timer() +end test.set_test_init_function(test_init) @@ -57,68 +100,205 @@ test.register_message_test( } ) +test.register_message_test("Refresh should read all necessary attributes", { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device)} + }, + { + channel = "zigbee", + direction = "send", + message = {mock_device.id, BasicInput.attributes.PresentValue:read(mock_device)} + }, +}) + +test.register_coroutine_test("panicAlarm should be triggered and cleared", function() + + local panic_report = IASZone.attributes.ZoneStatus.build_test_attr_report( + IASZone.attributes.ZoneStatus, + mock_device_panic, + 0x0002 + ) + + test.socket.zigbee:__queue_receive({ + mock_device_panic.id, + panic_report + }) + + test.socket.capability:__expect_send(mock_device_panic:generate_test_message("main", panicAlarm.panic({value = "panic", state_change = true}))) + + test.wait_for_events() + + local clear_report = IASZone.attributes.ZoneStatus.build_test_attr_report( + IASZone.attributes.ZoneStatus, + mock_device_panic, + 0x0001 + ) + test.socket.zigbee:__queue_receive({ + mock_device_panic.id, + clear_report + }) + + test.socket.capability:__expect_send(mock_device_panic:generate_test_message("main", panicAlarm.clear({value = "clear", state_change = true}))) + test.wait_for_events() + +end) + test.register_coroutine_test( "Battery Voltage test cases", function() - local battery_test_map = { - [33] = 100, - [32] = 100, - [27] = 50, - [26] = 30, - [23] = 10, - [15] = 0, - [10] = 0 - } + local battery_table = { + [33] = 100, + [32] = 100, + [27] = 50, + [26] = 30, + [23] = 10, + [15] = 0, + [10] = 0 + } + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + 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.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) + test.wait_for_events() - for voltage, batt_perc in pairs(battery_test_map) do + + for voltage, batt_perc in pairs(battery_table) do test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc)) ) - test.wait_for_events() + end end ) test.register_coroutine_test( - "Configure should configure all necessary attributes", + "added , init, and doConfigure should configure all necessary attributes", function() - test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.zigbee:__expect_send( - { - mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, - 30, - 21600, - 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, - BasicInput.attributes.PresentValue:configure_reporting(mock_device, 0, 600, 1) - } - ) - test.socket.zigbee:__expect_send( - { - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - BasicInput.ID) - } - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + 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.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) + + 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, + 0x20 + ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + BasicInput.ID, + 0x20 + ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30,21600, 1 ):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, BasicInput.attributes.PresentValue:configure_reporting(mock_device, 0,21600, 1 ):to_endpoint(0x20)}) + + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, OnOff.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 2):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, OnOff.ID, 0x8001, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 100):to_endpoint(0x20)}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, BasicInput.ID, 0x8000, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 65535):to_endpoint(0x20)}) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) +test.register_coroutine_test("info_changed for OnOff cluster attributes should run properly", +function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + { + preferences = { + ledColor = 1, + buttonDelay = 300, + } + } + )) + + local ledColor_msg = cluster_base.write_manufacturer_specific_attribute(mock_device,OnOff.ID, 0x8002, DEVELCO_MANUFACTURER_CODE, data_types.Enum8, 1) + ledColor_msg.body.zcl_header.frame_ctrl.value = 0x0C + ledColor_msg.address_header.dest_endpoint.value = 0x20 + test.socket.zigbee:__expect_send({mock_device.id, ledColor_msg}) + + local buttonDelay_msg = cluster_base.write_manufacturer_specific_attribute(mock_device,OnOff.ID, 0x8001, DEVELCO_MANUFACTURER_CODE, data_types.Uint16, 0x012C) + buttonDelay_msg.body.zcl_header.frame_ctrl.value = 0x0C + buttonDelay_msg.address_header.dest_endpoint.value = 0x20 + test.socket.zigbee:__expect_send({mock_device.id, buttonDelay_msg}) +end) + +test.register_coroutine_test(" Configuration and Switching to button-profile-panic-frient deviceProfile should be triggered", function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + { + preferences = { + panicButton = "0x002C" + } + } + )) + mock_device:expect_metadata_update({ profile = "button-profile-panic-frient" }) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, BasicInput.ID, 0x8000, DEVELCO_MANUFACTURER_CODE, data_types.Uint16,0x002C)}) + + local attributes = { + {attr = 0x8002, payload = 0x07D0, data_type = data_types.Uint16}, + {attr = 0x8003, payload = 0x07D0, data_type = data_types.Uint16}, + {attr = 0x8004, payload = 0x0A, data_type = data_types.Uint16}, + {attr = 0x8005, payload = 0, data_type = data_types.Enum8} + } + -- waiting for IASzone configuration execution + test.mock_time.advance_time(5) + for _, attr in ipairs(attributes) do + local msg = cluster_base.write_manufacturer_specific_attribute(mock_device,IASZone.ID, attr.attr, DEVELCO_MANUFACTURER_CODE, attr.data_type, attr.payload) + msg.address_header.dest_endpoint.value = 0x23 + test.socket.zigbee:__expect_send({mock_device.id, msg}) + end + -- Unable to check if the emit went through successfully due to the framework limitations in swapping mock device's deviceProfile + --test.socket.capability:__expect_send({mock_device.id, capabilities.panicAlarm.panicAlarm.clear({state_change = true})}) +end) + +test.register_coroutine_test("Switching from button-profile-panic-frient to button-profile-frient should work", function() + test.socket.device_lifecycle:__queue_receive(mock_device_panic:generate_info_changed( + { + preferences = { + panicButton = "0xFFFF" + }, + } + )) + mock_device_panic:expect_metadata_update({ profile = "button-profile-frient" }) + 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) + +test.register_coroutine_test("New preferences after switching the profile should work", function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device_panic:generate_info_changed( + { + preferences = { + buttonAlarmDelay = 1, + buttonCancelDelay = 300, + autoCancel = 20, + alarmBehavior = 1 + } + } + )) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8002,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 1)}) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8003,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 300)}) + test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8004,DEVELCO_MANUFACTURER_CODE,data_types.Uint16, 20)}) + 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) test.run_registered_tests() From 3abb12a1dff291490aad76dc4b543c265d3d3932 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 3 Sep 2025 12:10:24 -0700 Subject: [PATCH 088/449] WWSTCERT-7826 Shelly Wave Motion --- drivers/SmartThings/zwave-sensor/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 0d04f7aff9..7a26909ecb 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -542,6 +542,12 @@ zwaveManufacturer: productType: 0x0000 productId: 0x0001 deviceProfileName: base-water + - id: 1120/256/130 + deviceLabel: Shelly Wave Motion + manufacturerId: 0x0460 + productId: 0x0082 + productType: 0x0100 + deviceProfileName: motion-battery-illuminance zwaveGeneric: - id: "GenericSensorAlarm" deviceLabel: Z-Wave Sensor From e7afcc79b43f527cb25218d64c65fecd471e1345 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 3 Sep 2025 12:05:53 -0700 Subject: [PATCH 089/449] WWSTCERT-7836 Aug. Winkhaus SE Funkkontakt FM.V.ZB --- drivers/SmartThings/zigbee-contact/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index dbb88f33a6..172361da3b 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -194,6 +194,11 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RVS01031Z deviceProfileName: thirdreality-multi-sensor + - id: "Aug. Winkhaus SE/FM.V.ZB" + deviceLabel: Funkkontakt FM.V.ZB + manufacturer: Aug. Winkhaus SE + model: FM.V.ZB + deviceProfileName: contact-battery-profile zigbeeGeneric: - id: "contact-generic" deviceLabel: "Zigbee Contact Sensor" From 8925b26be8f2369ac391c6a9207961256ffc0699 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:13:39 -0500 Subject: [PATCH 090/449] add missed optional capabilities to modular profile (#2370) --- .../matter-thermostat/profiles/thermostat-modular.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml index 19dbc88e37..1e7bd97dc7 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml @@ -9,6 +9,12 @@ components: - id: fanMode version: 1 optional: true + - id: fanOscillationMode + version: 1 + optional: true + - id: windMode + version: 1 + optional: true - id: thermostatFanMode version: 1 optional: true From d93a2116b6ba4d7d6c70ee63a2c760a0a297db8b Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 4 Sep 2025 14:36:14 -0500 Subject: [PATCH 091/449] change generic fingerprints to be more minimal capability versions --- .../matter-thermostat/fingerprints.yml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index e25443d49c..be1d3c1464 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -66,55 +66,55 @@ matterGeneric: deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - deviceProfileName: thermostat + deviceProfileName: thermostat-nostate-nobattery - id: "matter/hvac/thermostat" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - deviceProfileName: thermostat + deviceProfileName: thermostat-nostate-nobattery - id: "matter/hvac/heatcool/humidity" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x0307 # Humidity Sensor - deviceProfileName: thermostat-humidity + deviceProfileName: thermostat-humidity-nostate-nobattery - id: "matter/hvac/thermostat/humidity" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x0307 # Humidity Sensor - deviceProfileName: thermostat-humidity + deviceProfileName: thermostat-humidity-nostate-nobattery - id: "matter/hvac/heatcool/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x002B # Fan - deviceProfileName: thermostat-fan + deviceProfileName: thermostat-fan-nostate-nobattery - id: "matter/hvac/thermostat/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x002B # Fan - deviceProfileName: thermostat-fan + deviceProfileName: thermostat-fan-nostate-nobattery - id: "matter/hvac/heatcool/humidity/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0300 # HVAC Heating/Cooling Unit - id: 0x0307 # Humidity Sensor - id: 0x002B # Fan - deviceProfileName: thermostat-humidity-fan + deviceProfileName: thermostat-humidity-fan-nostate-nobattery - id: "matter/hvac/thermostat/humidity/fan" deviceLabel: Matter Thermostat deviceTypes: - id: 0x0301 # Thermostat - id: 0x0307 # Humidity Sensor - id: 0x002B # Fan - deviceProfileName: thermostat-humidity-fan + deviceProfileName: thermostat-humidity-fan-nostate-nobattery - id: "matter/room-air-conditioner" deviceLabel: Matter Room Air Conditioner deviceTypes: - id: 0x0072 - deviceProfileName: room-air-conditioner + deviceProfileName: room-air-conditioner-fan-heating-cooling-nostate - id: "matter/fan" deviceLabel: Matter Fan deviceTypes: @@ -124,7 +124,7 @@ matterGeneric: deviceLabel: Matter Air Purifier deviceTypes: - id: 0x002D # Air Purifier - deviceProfileName: air-purifier-hepa-ac-wind + deviceProfileName: air-purifier - id: "matter/air-purifier/quality-sensor" deviceLabel: Matter Air Purifier & Quality Sensor deviceTypes: From c2c8cb6ed5c71bc41664afec55d5c1c3d5aa34ee Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:22:34 +0900 Subject: [PATCH 092/449] Add Ultraloq-Bolt to new-matter-lock driver (#2369) Signed-off-by: Hunsup Jung --- drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 70e72cf61f..882239ec49 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -38,6 +38,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2801}, -- AQARA, U300 {0x115f, 0x2807}, -- AQARA, U200 Lite {0x147F, 0x0001}, -- U-tec + {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 {0x101D, 0x8110}, -- Yale, New Lock {0x1533, 0x0001}, -- eufy, E31 From fe59fe3c11567f81b6d3f58d2b4edf66f90373d6 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 5 Sep 2025 12:33:11 -0700 Subject: [PATCH 093/449] WWSTCERT-7839 OSRAM MATTER PLUG UK --- 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 aa95b8d4dc..2aba4b5af8 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -797,6 +797,12 @@ matterManufacturer: vendorId: 0x137F productId: 0x027B deviceProfileName: plug-binary +#Osram + - id: "4489/2564" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0x0A04 + deviceProfileName: plug-binary #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From e4bddba54b985cd73add1981d6b4494ded06b5b1 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Tue, 9 Sep 2025 14:16:52 +0200 Subject: [PATCH 094/449] Add support for MOSZB-153 --- .../zigbee-motion-sensor/fingerprints.yml | 5 + ...frient-motion-temp-illuminance-battery.yml | 57 +++ .../zigbee-motion-sensor/src/frient/init.lua | 3 +- .../test/test_frient_motion_sensor2_pet.lua | 398 ++++++++++++++++++ 4 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua diff --git a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml index c1a3b0fda3..28aaea8d33 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml @@ -168,6 +168,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: MOSZB-141 deviceProfileName: frient-motion-battery + - id: frientA/S/153 + deviceLabel: frient Motion Sensor 2 Pet + manufacturer: frient A/S + model: MOSZB-153 + deviceProfileName: frient-motion-temp-illuminance-battery - id: Compacta/ZBMS3-1 deviceLabel: Smartenit Motion Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml new file mode 100644 index 0000000000..1c8f50f822 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/profiles/frient-motion-temp-illuminance-battery.yml @@ -0,0 +1,57 @@ +name: frient-motion-temp-illuminance-battery +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Motion Turn-Off Delay (s)" + name: occupiedToUnoccupiedD + description: "Delay in seconds to report after no motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 + - title: "Motion Turn-On Delay (s)" + name: unoccupiedToOccupiedD + description: "Delay in seconds to report after motion is detected" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 0 + - title: "Movement Threshold in Turn-On Delay" + name: unoccupiedToOccupiedT + description: "Number of movements to detect before reporting motion during the Motion Turn-On Delay" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 254 + default: 1 diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua index 0278350cbe..43ce315ab2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua @@ -37,7 +37,8 @@ local ILLUMINANCE_ENDPOINT = 0x27 local FRIENT_DEVICE_FINGERPRINTS = { { mfr = "frient A/S", model = "MOSZB-140"}, - { mfr = "frient A/S", model = "MOSZB-141"} + { mfr = "frient A/S", model = "MOSZB-141"}, + { mfr = "frient A/S", model = "MOSZB-153"} } local function can_handle_frient_motion_sensor(opts, driver, device) 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 new file mode 100644 index 0000000000..0f142ea6cb --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua @@ -0,0 +1,398 @@ +-- Copyright 2025 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 base64 = require "base64" +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local IASZone = zcl_clusters.IASZone +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local IlluminanceMeasurement = zcl_clusters.IlluminanceMeasurement +local OccupancySensing = zcl_clusters.OccupancySensing +local PowerConfiguration = zcl_clusters.PowerConfiguration +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement + +local capabilities = require "st.capabilities" + +local IASZONE_ENDPOINT = 0x23 +local ILLUMINANCE_ENDPOINT = 0x27 +local OCCUPANCY_ENDPOINT = 0x22 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 + +local DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY = 240 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY = 0 +local DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD = 0 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-motion-temp-illuminance-battery.yml"), + zigbee_endpoints = { + [0x22] = { + id = 0x22, + manufacturer = "frient A/S", + model = "MOSZB-153", + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x000f, 0x0020, 0x0500 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + }, + [0x27] = { + id = 0x27, + server_clusters = { 0x0000, 0x0003, 0x0400 } + }, + [0x28] = { + id = 0x28, + server_clusters = { 0x0000, 0x0003, 0x0406 } + }, + [0x29] = { + id = 0x29, + server_clusters = { 0x0000, 0x0003, 0x0406 } + } + } + } +) + +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_coroutine_test( + "Motion inactive clear states when the device is added", function() + test.socket.matter:__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.motionSensor.motion.inactive()) + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Illuminance report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 21370) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({ value = 137 })) + } + } +) + +-- test.register_coroutine_test( +-- "Health check should check all relevant attributes", +-- function() +-- test.wait_for_events() +-- test.mock_time.advance_time(50000) +-- test.socket.zigbee:__set_channel_ordering("relaxed") +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) +-- } +-- ) +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) +-- } +-- ) +-- end, +-- { +-- test_init = function() +-- test.mock_device.add_test_device(mock_device) +-- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") +-- end +-- } +-- ) + +test.register_coroutine_test( + "Refresh should read all necessary 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, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT)}) + test.socket.zigbee:__expect_send({mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT)}) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + ) + + test.wait_for_events() + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 1 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + OccupancySensing.ID, + OCCUPANCY_ENDPOINT + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:configure_reporting( + mock_device, + 0, + 3600 + ):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + 10 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IlluminanceMeasurement.ID, + ILLUMINANCE_ENDPOINT + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 10, + 3600, + 0x2711 + ):to_endpoint(ILLUMINANCE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, DEFAULT_OCCUPIED_TO_UNOCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_DELAY) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, DEFAULT_UNOCCUPIED_TO_OCCUPIED_THRESHOLD) + :to_endpoint(OCCUPANCY_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Temperature Sensitivity, Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + occupiedToUnoccupiedD = 200, + unoccupiedToOccupiedD = 1, + unoccupiedToOccupiedT = 2 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedDelay:write(mock_device, 1):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIRUnoccupiedToOccupiedThreshold:write(mock_device, 2):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ mock_device.id, + OccupancySensing.attributes.PIROccupiedToUnoccupiedDelay:write(mock_device, 200):to_endpoint(OCCUPANCY_ENDPOINT) + }) + + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + end +) + +test.run_registered_tests() From af611238dad0a31834975a395df320c5dc6be004 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 10 Jul 2025 15:48:59 -0700 Subject: [PATCH 095/449] WWSTCERT-6975 SmartWings Window Covering --- .../matter-window-covering/fingerprints.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d494e93749..ca557fd2e4 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -42,6 +42,12 @@ matterManufacturer: vendorId: 0x1500 productId: 0x2711 deviceProfileName: window-covering-battery +# SmartWings + - id: "5231/4097" + deviceLabel: SmartWings Window Covering + vendorId: 0x146F + productId: 0x1001 + deviceProfileName: window-covering-battery #Zemismart - id: "Zemismart MT01 Slide Curtain" deviceLabel: Zemismart MT01 Slide Curtain @@ -93,7 +99,7 @@ matterManufacturer: deviceLabel: WISTAR WSERD24 Smart Tubular Motor vendorId: 0x1457 productId: 0x0004 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/5" deviceLabel: WISTAR WSERD40-B Smart Tubular Motor vendorId: 0x1457 @@ -108,7 +114,7 @@ matterManufacturer: deviceLabel: WISTAR WSERD40-T Smart Tubular Motor vendorId: 0x1457 productId: 0x0007 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/8" deviceLabel: WISTAR WSERD50-B Smart Tubular Motor vendorId: 0x1457 @@ -123,12 +129,12 @@ matterManufacturer: deviceLabel: WISTAR WSERD50-T Smart Tubular Motor vendorId: 0x1457 productId: 0x0010 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/19" deviceLabel: WISTAR WSER60 Smart Tubular Motor vendorId: 0x1457 productId: 0x0013 - deviceProfileName: window-covering + deviceProfileName: window-covering - id: "5207/17" deviceLabel: WISTAR WSER40 Smart Tubular Motor vendorId: 0x1457 From 6360180d2513e8d72163d0336d9209328d6caa0c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 9 Sep 2025 11:44:47 -0700 Subject: [PATCH 096/449] Revert "Merge pull request #2374 from SmartThingsCommunity/new_device/WWSTCERT-7826" This reverts commit 2c7dbcf9c51d52625a1006975dfe83b76cbb90d3, reversing changes made to bf8461d8696115223492b2525050da51ae107bbb. --- drivers/SmartThings/zwave-sensor/fingerprints.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 7a26909ecb..0d04f7aff9 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -542,12 +542,6 @@ zwaveManufacturer: productType: 0x0000 productId: 0x0001 deviceProfileName: base-water - - id: 1120/256/130 - deviceLabel: Shelly Wave Motion - manufacturerId: 0x0460 - productId: 0x0082 - productType: 0x0100 - deviceProfileName: motion-battery-illuminance zwaveGeneric: - id: "GenericSensorAlarm" deviceLabel: Z-Wave Sensor From 2602ad32bafa0d1162234ce6fa5c3355f38ba90e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 9 Sep 2025 12:00:19 -0700 Subject: [PATCH 097/449] WWSTCERT-7852 Griesser MSM-1 --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index ca557fd2e4..d7785021c1 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -30,6 +30,12 @@ matterManufacturer: vendorId: 0x130A productId: 0x0060 deviceProfileName: window-covering +# Griesser + - id: "5435/14337" + deviceLabel: MSM-1 + vendorId: 0x153B + productId: 0x3801 + deviceProfileName: window-covering-tilt # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain From a6f95d8e8698d7d5b88ecabcd8bb130598b595d1 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 5 Sep 2025 12:40:11 -0700 Subject: [PATCH 098/449] WWSTCERT-7847 Osram SMART MAT P40 RGBW 827 FR E27 --- 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 2aba4b5af8..e5b25558a1 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -803,6 +803,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0A04 deviceProfileName: plug-binary + - id: "4489/2757" + deviceLabel: SMART MAT P40 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC5 + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 2b632b4d3438baedec20c457e8231e066badd899 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 10 Sep 2025 12:23:00 -0500 Subject: [PATCH 099/449] add modular profile for new matter lock devices (#2366) --- .../lock-modular-embedded-unlatch.yml | 131 +++++ .../matter-lock/profiles/lock-modular.yml | 31 + .../matter-lock/src/new-matter-lock/init.lua | 138 ++++- .../src/test/test_aqara_matter_lock.lua | 19 +- .../src/test/test_matter_lock_modular.lua | 532 ++++++++++++++++++ .../src/test/test_matter_lock_unlatch.lua | 19 +- .../src/test/test_new_matter_lock.lua | 21 +- .../src/test/test_new_matter_lock_battery.lua | 113 ++-- 8 files changed, 918 insertions(+), 86 deletions(-) create mode 100644 drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml create mode 100644 drivers/SmartThings/matter-lock/profiles/lock-modular.yml create mode 100644 drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml new file mode 100644 index 0000000000..4ea6ba1e0d --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -0,0 +1,131 @@ +name: lock-modular-embedded-unlatch +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + optional: true + - id: lockCredentials + version: 1 + optional: true + - id: lockSchedules + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock +deviceConfig: + dashboard: + states: + - component: main + capability: lock + version: 1 + actions: + - component: main + capability: lock + version: 1 + visibleCondition: { + "capability": "lock", + "version": "1", + "component": "main", + "value": "lock.value", + "operator": "DOES_NOT_EQUAL", + "operand": "unlatched" + } + detailView: + - component: main + capability: lock + version: 1 + values: + - key: lock.value + alternatives: + - key: locked + type: inactive + value: '{{i18n.attributes.lock.i18n.value.locked.label}}' + - key: unlocked + value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}' + - key: unlatched + value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}' + - key: not fully locked + value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}' + patch: + - op: add + path: /1 + value: + capability: lock + version: 1 + component: main + label: '{{i18n.commands.unlatch.label}}' + displayType: pushButton + pushButton: + command: unlatch + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 + - component: main + capability: battery + version: 1 + - component: main + capability: batteryLevel + version: 1 + automation: + conditions: + - component: main + capability: lock + version: 1 + values: + - key: lock.value + alternatives: + - key: locked + type: inactive + value: '{{i18n.attributes.lock.i18n.value.locked.label}}' + - key: unlocked + value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}' + - key: unlatched + value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}' + - key: not fully locked + value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}' + - component: main + capability: lockAlarm + version: 1 + - component: main + capability: remoteControlStatus + version: 1 + - component: main + capability: battery + version: 1 + - component: main + capability: batteryLevel + version: 1 + actions: + - component: main + capability: lock + version: 1 + values: + - key: '{{enumCommands}}' + alternatives: + - key: lock + type: inactive + value: '{{i18n.commands.lock.label}}' + - key: unlock + value: '{{i18n.commands.unlock.label}}' + - key: unlatch + value: '{{i18n.commands.unlatch.label}}' diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml new file mode 100644 index 0000000000..576695f873 --- /dev/null +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -0,0 +1,31 @@ +name: lock-modular +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockAlarm + version: 1 + - id: remoteControlStatus + version: 1 + - id: lockUsers + version: 1 + optional: true + - id: lockCredentials + version: 1 + optional: true + - id: lockSchedules + version: 1 + optional: true + - id: battery + version: 1 + optional: true + - id: batteryLevel + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 882239ec49..aad7046a3d 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -56,7 +56,15 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x10E1, 0x2002} -- VDA } -local PROFILE_BASE_NAME = "__profile_base_name" +local battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +local profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", +} local subscribed_attributes = { [capabilities.lock.ID] = { @@ -149,15 +157,64 @@ local function device_init(driver, device) local function device_added(driver, device) device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + device:send(clusters.PowerSource.attributes.AttributeList:read(device)) + else + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, { persist = true }) + end end -local function do_configure(driver, device) +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, 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 + 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}) + device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) +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 battery_eps = device:get_endpoints(PowerSource.ID, {feature_bitmap = PowerSource.types.PowerSourceFeature.BATTERY}) local profile_name = "lock" if #user_eps > 0 then @@ -175,15 +232,16 @@ local function do_configure(driver, device) else device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) end - if #battery_eps > 0 then - device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(req) - else - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) + + 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 local function info_changed(driver, device, event, args) @@ -209,6 +267,33 @@ local function info_changed(driver, device, event, args) device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is madatory 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) + if profiling_data_still_required(device) then return end + + if version.api >= 15 and version.rpc >= 9 then + match_profile_modular(driver, device) + else + match_profile_switch(driver, device) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) +end + +local function driver_switched(driver, device) + match_profile(driver, device) +end + -- This function check busy_state and if busy_state is false, set it to true(current time) local function check_busy_state(device) local c_time = os.time() @@ -398,27 +483,23 @@ end -- Power Source Attribute List -- --------------------------------- 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) is present. + -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. if attr.value == 0x0C then - support_battery_percentage = true - end - if 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 support_battery_percentage then - profile_name = profile_name .. "-battery" - elseif support_battery_level then - profile_name = profile_name .. "-batteryLevel" + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, { persist = true }) + match_profile(driver, device) + return + elseif attr.value == 0x0E then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL, { persist = true }) + match_profile(driver, device) + return 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 + + -- neither BatChargeLevel nor BatPercentRemaining were found. Re-profiling without battery. + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, { persist = true }) + match_profile(driver, device) end ------------------------------- @@ -2086,6 +2167,7 @@ local new_matter_lock_handler = { added = device_added, doConfigure = do_configure, infoChanged = info_changed, + driverSwitched = driver_switched }, matter_handlers = { attr = { 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 7427ad31d0..41ab9840fb 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 @@ -13,6 +13,7 @@ -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" @@ -54,12 +55,7 @@ local mock_device = test.mock_device.build_test_matter_device({ local function test_init() test.disable_startup_messages() - 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})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + -- subscribe request local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.DoorLock.attributes.OperatingMode:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) @@ -70,7 +66,16 @@ local function test_init() subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- add test device + 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})) + ) + 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" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) 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 new file mode 100644 index 0000000000..5dd117a70e --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua @@ -0,0 +1,532 @@ +-- Copyright 2025 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 test = require "integration_test" +local capabilities = require "st.capabilities" +test.add_package_capability("lockAlarm.yml") +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" + +local DoorLock = clusters.DoorLock + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock.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_unlatch = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-unlatch.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 = 0x1000, -- UNLATCH + }, + { + 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_user_pin = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-user-pin.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 = 0x0181, -- PIN & USR & COTA + }, + { + 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_user_pin_schedule_unlatch = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-user-pin-schedule-unlatch.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 = 0x1591, -- PIN & USR & COTA & WDSCH & YDSCH & UNLATCH + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local function test_init() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) + -- add test device + test.mock_device.add_test_device(mock_device) + -- 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})) + ) + test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + 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({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_unlatch() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_unlatch) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_user_pin() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) + -- add test device + test.mock_device.add_test_device(mock_device_user_pin) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_user_pin_schedule_unlatch() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) + -- add test device + test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Test lock profile change when attributes related to BAT feature is not available.", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + 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 = {{"main", {}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change when BatChargeLevel attribute is available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + 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 = {{"main", {"batteryLevel"}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + 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 = {{"main", {"battery"}}} }) + end +) + +test.register_coroutine_test( + "Test modular lock profile change with unlatch when attributes related to BAT feature is not available.", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test lock-unlatch profile change with unlatch when BatChargeLevel attribute is available", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"batteryLevel"}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test modular lock profile change with unlatch when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"battery"}}} }) + end, + { test_init = test_init_unlatch } +) + +test.register_coroutine_test( + "Test lock-user-pin profile change when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_user_pin.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_user_pin, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device_user_pin:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "battery"}}} }) + end, + { test_init = test_init_user_pin } +) + +test.register_coroutine_test( + "Test modular lock profile change with user, pin. schedule, and unlatch supported when BatChargeLevel and BatPercentRemaining attributes are available", + function() + test.socket.matter:__queue_receive( + { + mock_device_user_pin_schedule_unlatch.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_user_pin_schedule_unlatch, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}} }) + + end, + { test_init = test_init_user_pin_schedule_unlatch } +) + +test.run_registered_tests() 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 92adc6c6fd..421134ec2a 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 @@ -13,6 +13,7 @@ -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" @@ -55,23 +56,27 @@ local mock_device = test.mock_device.build_test_matter_device({ local function test_init() test.disable_startup_messages() + -- subscribe_request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm: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})) ) 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.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, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "lock-unlatch" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) ) + mock_device:expect_metadata_update({ profile = "lock-unlatch" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 442593b188..6067d056ff 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 @@ -13,6 +13,7 @@ -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" @@ -56,12 +57,7 @@ local mock_device = test.mock_device.build_test_matter_device({ local function test_init() test.disable_startup_messages() - 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})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + -- subscribe request 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)) @@ -74,13 +70,22 @@ 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})) + ) + 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 = "lock-user-pin" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.set_test_init_function(test_init) 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 d8657ccae3..d58bfb1bdd 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 @@ -13,6 +13,7 @@ -- limitations under the License. local test = require "integration_test" +test.set_rpc_version(0) local capabilities = require "st.capabilities" test.add_package_capability("lockAlarm.yml") local clusters = require "st.matter.clusters" @@ -175,54 +176,51 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter local function test_init() test.disable_startup_messages() - 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})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(DoorLock.attributes.OperatingMode: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" }) + -- 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.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) + 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({ provisioning_state = "PROVISIONED" }) end local function test_init_unlatch() test.disable_startup_messages() - test.mock_device.add_test_device(mock_device_unlatch) - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) - test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_unlatch)) - test.socket["matter"]:__expect_send({mock_device_unlatch.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_unlatch) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "added" }) test.socket.capability:__expect_send( - mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + mock_device_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) test.socket.matter:__expect_send({mock_device_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_unlatch.id, "doConfigure" }) mock_device_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin() test.disable_startup_messages() - test.mock_device.add_test_device(mock_device_user_pin) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) - test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin)) @@ -233,23 +231,24 @@ local function test_init_user_pin() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin)) - test.socket["matter"]:__expect_send({mock_device_user_pin.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_user_pin) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "added" }) test.socket.capability:__expect_send( - mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + mock_device_user_pin:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) test.socket.matter:__expect_send({mock_device_user_pin.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin.id, "doConfigure" }) mock_device_user_pin:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_user_pin_schedule_unlatch() test.disable_startup_messages() - test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) - test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) + -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_user_pin_schedule_unlatch) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_user_pin_schedule_unlatch)) @@ -262,12 +261,18 @@ local function test_init_user_pin_schedule_unlatch() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_user_pin_schedule_unlatch)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_user_pin_schedule_unlatch)) - test.socket["matter"]:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_user_pin_schedule_unlatch) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "added" }) test.socket.capability:__expect_send( - mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "init" }) + test.socket.matter:__expect_send({mock_device_user_pin_schedule_unlatch.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_user_pin_schedule_unlatch.id, "doConfigure" }) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -293,6 +298,9 @@ test.register_coroutine_test( }) } ) + 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" }) end ) @@ -318,6 +326,9 @@ test.register_coroutine_test( }) } ) + 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-batteryLevel" }) end ) @@ -344,6 +355,9 @@ test.register_coroutine_test( }) } ) + 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-battery" }) end ) @@ -368,6 +382,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch" }) end, { test_init = test_init_unlatch } @@ -394,6 +411,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch-batteryLevel" }) end, { test_init = test_init_unlatch } @@ -421,6 +441,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_unlatch:expect_metadata_update({ profile = "lock-unlatch-battery" }) end, { test_init = test_init_unlatch } @@ -446,6 +469,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin" }) end, { test_init = test_init_user_pin } @@ -472,6 +498,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin-batteryLevel" }) end, { test_init = test_init_user_pin } @@ -499,6 +528,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) mock_device_user_pin:expect_metadata_update({ profile = "lock-user-pin-battery" }) end, { test_init = test_init_user_pin } @@ -524,6 +556,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch" }) end, { test_init = test_init_user_pin_schedule_unlatch } @@ -550,6 +585,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch-batteryLevel" }) end, { test_init = test_init_user_pin_schedule_unlatch } @@ -577,6 +615,9 @@ test.register_coroutine_test( }) } ) + test.socket.capability:__expect_send( + mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) mock_device_user_pin_schedule_unlatch:expect_metadata_update({ profile = "lock-user-pin-schedule-unlatch-battery" }) end, { test_init = test_init_user_pin_schedule_unlatch } From ba45af73f60819ad22c7930541c488fe1f30c34c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 25 Aug 2025 13:45:31 -0700 Subject: [PATCH 100/449] WWSTCERT-7387 Intecular InvisOutlet (#2318) * WWSTCERT-7387 Intecular InvisOutlet * update profile --- 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 13511bb728..aa95b8d4dc 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -455,6 +455,12 @@ matterManufacturer: vendorId: 0x100B productId: 0x0800 deviceProfileName: light-level-colorTemperature +# Intecular + - id: "5226/32769" + deviceLabel: InvisOutlet + vendorId: 0x146A + productId: 0x8001 + deviceProfileName: plug-binary #Ledvance - id: "4489/843" From 97b1f09e946546163fa82a7293ac29148f8ef23c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 25 Aug 2025 13:48:38 -0700 Subject: [PATCH 101/449] Merge pull request #2353 from SmartThingsCommunity/new_device/WWSTCERT-7620 WWSTCERT-7620 ULTRALOQ Bolt Smart Matter Door Lock --- 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 7ad02e6ac7..36b7d115f1 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -99,6 +99,11 @@ matterManufacturer: vendorId: 0x147F productId: 0x0001 deviceProfileName: lock-user-pin-battery + - id: "5247/8" + deviceLabel: ULTRALOQ Bolt Smart Matter Door Lock + vendorId: 0x147F + productId: 0x0008 + deviceProfileName: lock-user-pin-battery matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock From ec6ecde833aad124014f936a4d70d1152f584cfb Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 10 Sep 2025 11:05:11 -0700 Subject: [PATCH 102/449] Merge pull request #2355 from SmartThingsCommunity/new_device/WWSTCERT-7582 WWSTCERT-7582 Sense by MACO | Window T&T --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3753e8b23b..8072dc6a84 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -102,6 +102,11 @@ matterManufacturer: vendorId: 0x1547 productId: 0x03ED deviceProfileName: contact-battery + - id: "5447/1002" + deviceLabel: Sense by MACO | Window TT + vendorId: 0x1547 + productId: 0x03EA + deviceProfileName: contact-battery # Meross - id: "4933/16897" deviceLabel: Smart Presence Sensor From 9feb81563fb72b8cfd53680fcb119fa41608b79f Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 28 Aug 2025 08:38:08 -0700 Subject: [PATCH 103/449] Merge pull request #2367 from SmartThingsCommunity/new_device/WWSTCERT-7683 WWSTCERT-7683 First Alert SMCO410 --- drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml index 6a73e78428..39b541becb 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml +++ b/drivers/SmartThings/zwave-smoke-alarm/fingerprints.yml @@ -63,3 +63,9 @@ zwaveManufacturer: productType: 0x0004 productId: 0x0003 deviceProfileName: co-battery + - id: "1051/1/1040" + deviceLabel: SMCO410 + manufacturerId: 0x041B + productId: 0x0410 + productType: 0x0001 + deviceProfileName: smoke-co-battery From ae92a0b8bc1e2882d0c2fe65079ccc9666e1e447 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 3 Sep 2025 12:17:25 -0700 Subject: [PATCH 104/449] Merge pull request #2373 from SmartThingsCommunity/new_device/WWSTCERT-7836 WWSTCERT-7836 Aug. Winkhaus SE Funkkontakt FM.V.ZB --- drivers/SmartThings/zigbee-contact/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index dbb88f33a6..172361da3b 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -194,6 +194,11 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RVS01031Z deviceProfileName: thirdreality-multi-sensor + - id: "Aug. Winkhaus SE/FM.V.ZB" + deviceLabel: Funkkontakt FM.V.ZB + manufacturer: Aug. Winkhaus SE + model: FM.V.ZB + deviceProfileName: contact-battery-profile zigbeeGeneric: - id: "contact-generic" deviceLabel: "Zigbee Contact Sensor" From 6add2bf74052125d30d3de196af78ea7621db481 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 9 Sep 2025 12:02:02 -0700 Subject: [PATCH 105/449] Merge pull request #2377 from SmartThingsCommunity/new_device/WWSTCERT-7839 WWSTCERT-7839 OSRAM MATTER PLUG UK --- 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 aa95b8d4dc..2aba4b5af8 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -797,6 +797,12 @@ matterManufacturer: vendorId: 0x137F productId: 0x027B deviceProfileName: plug-binary +#Osram + - id: "4489/2564" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0x0A04 + deviceProfileName: plug-binary #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 22e20fcf2f2a6e13ac136987f68106bb01ecde94 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 9 Sep 2025 12:37:56 -0700 Subject: [PATCH 106/449] Merge pull request #2378 from SmartThingsCommunity/new_device/WWSTCERT-7847 WWSTCERT-7847 Osram SMART MAT P40 RGBW 827 FR E27 --- 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 2aba4b5af8..e5b25558a1 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -803,6 +803,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0A04 deviceProfileName: plug-binary + - id: "4489/2757" + deviceLabel: SMART MAT P40 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC5 + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From b68eb1e2224987eac0f063901be6a35407354d0f Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 9 Sep 2025 12:40:20 -0700 Subject: [PATCH 107/449] Merge pull request #2383 from SmartThingsCommunity/new_device/WWSTCERT-7852 WWSTCERT-7852 Griesser MSM-1 --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d494e93749..4734907958 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -30,6 +30,12 @@ matterManufacturer: vendorId: 0x130A productId: 0x0060 deviceProfileName: window-covering +# Griesser + - id: "5435/14337" + deviceLabel: MSM-1 + vendorId: 0x153B + productId: 0x3801 + deviceProfileName: window-covering-tilt # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain From 856f35f4acdfdc78dddf49f4c58dac190fae4288 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Wed, 10 Sep 2025 14:10:26 -0500 Subject: [PATCH 108/449] philips-hue: Split mapping of hue resource IDs by bridge --- .../philips-hue/src/disco/init.lua | 7 +++++- .../handlers/lifecycle_handlers/button.lua | 14 ++++++----- .../handlers/lifecycle_handlers/contact.lua | 12 ++++++---- .../src/handlers/lifecycle_handlers/init.lua | 3 ++- .../src/handlers/lifecycle_handlers/light.lua | 8 ++++--- .../handlers/lifecycle_handlers/motion.lua | 14 ++++++----- .../philips-hue/src/hue_driver_template.lua | 4 ++-- .../philips-hue/src/utils/grouped_utils.lua | 24 +++++++++++++------ .../src/utils/hue_bridge_utils.lua | 7 +++--- .../hue_multi_service_device_utils/sensor.lua | 5 +++- .../philips-hue/src/utils/init.lua | 22 +++++++++++++++++ 11 files changed, 85 insertions(+), 35 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/disco/init.lua b/drivers/SmartThings/philips-hue/src/disco/init.lua index 0746e13942..81b74ae9ef 100644 --- a/drivers/SmartThings/philips-hue/src/disco/init.lua +++ b/drivers/SmartThings/philips-hue/src/disco/init.lua @@ -334,8 +334,13 @@ function HueDiscovery.handle_discovered_child_device(driver, bridge_network_id, end for _, svc_info in ipairs(primary_services[primary_service_type]) do + if driver:get_device_by_dni(v1_dni) then + return + end local v2_resource_id = svc_info.rid or "" - if driver:get_device_by_dni(v1_dni) or driver.hue_identifier_to_device_record[v2_resource_id] then return end + if (driver.hue_identifier_to_device_record_by_bridge[bridge_network_id] or {})[v2_resource_id] then + return + end end local api_instance = diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua index d1d054e425..6c1d41dbb8 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -52,8 +52,9 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc end local button_rid_to_index_map = {} + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} if button_info.button then - driver.hue_identifier_to_device_record[button_info.id] = device + hue_id_to_device[button_info.id] = device button_rid_to_index_map[button_info.id] = 1 end @@ -65,7 +66,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc local button_id = button_info[button_id_key] if button and button_id then - driver.hue_identifier_to_device_record[button_id] = device + hue_id_to_device[button_id] = device button_rid_to_index_map[button_id] = var local supported_button_values = utils.get_supported_button_values(button.event_values) @@ -86,7 +87,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc end if button_info.power_id then - driver.hue_identifier_to_device_record[button_info.power_id] = device + hue_id_to_device[button_info.power_id] = device end log.debug(st_utils.stringify_table(button_rid_to_index_map, "button index map", true)) @@ -98,7 +99,7 @@ function ButtonLifecycleHandlers.added(driver, device, parent_device_id, resourc device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_button_resource_id] = device + hue_id_to_device[device_button_resource_id] = device end ---@param driver HueDriver @@ -114,8 +115,9 @@ function ButtonLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_button_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_button_resource_id] then - driver.hue_identifier_to_device_record[device_button_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_button_resource_id] then + hue_id_to_device[device_button_resource_id] = device end local button_info, err button_info = Discovery.device_state_disco_cache[device_button_resource_id] diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua index ea5f1ceac2..eaef1a2ccd 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/contact.lua @@ -51,8 +51,9 @@ function ContactLifecycleHandlers.added(driver, device, parent_device_id, resour return end - driver.hue_identifier_to_device_record[sensor_info.power_id] = device - driver.hue_identifier_to_device_record[sensor_info.tamper_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + hue_id_to_device[sensor_info.power_id] = device + hue_id_to_device[sensor_info.tamper_id] = device device:set_field(Fields.DEVICE_TYPE, HueDeviceTypes.CONTACT, { persist = true }) device:set_field(Fields.HUE_DEVICE_ID, sensor_info.hue_device_id, { persist = true }) @@ -61,7 +62,7 @@ function ContactLifecycleHandlers.added(driver, device, parent_device_id, resour device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + hue_id_to_device[device_sensor_resource_id] = device end ---@param driver HueDriver @@ -77,8 +78,9 @@ function ContactLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_sensor_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_sensor_resource_id] then - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_sensor_resource_id] then + hue_id_to_device[device_sensor_resource_id] = device end local sensor_info, err sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua index c9d413841d..7724246595 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/init.lua @@ -93,8 +93,9 @@ function LifecycleHandlers.device_added(driver, device, ...) if device_type ~= HueDeviceTypes.BRIDGE then ---@cast device HueChildDevice local resource_id = utils.get_hue_rid(device) + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} if resource_id then - driver.hue_identifier_to_device_record[resource_id] = device + hue_id_to_device[resource_id] = device end local resource_state_known = (Discovery.device_state_disco_cache[resource_id] ~= nil) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua index 00258e5722..edbfb02944 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/light.lua @@ -190,7 +190,8 @@ function LightLifecycleHandlers.added(driver, device, parent_device_id, resource device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_light_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + hue_id_to_device[device_light_resource_id] = device -- the refresh handler adds lights that don't have a fully initialized bridge to a queue. driver:inject_capability_command(device, { @@ -218,8 +219,9 @@ function LightLifecycleHandlers.init(driver, device) device.device_network_id local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_light_resource_id] then - driver.hue_identifier_to_device_record[device_light_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_light_resource_id] then + hue_id_to_device[device_light_resource_id] = device end local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} if not svc_rids_for_device[device_light_resource_id] then diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua index ceb301dcd4..90b070ff3b 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/motion.lua @@ -50,10 +50,11 @@ function MotionLifecycleHandlers.added(driver, device, parent_device_id, resourc }) return end + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} - driver.hue_identifier_to_device_record[sensor_info.power_id] = device - driver.hue_identifier_to_device_record[sensor_info.temperature_id] = device - driver.hue_identifier_to_device_record[sensor_info.light_level_id] = device + hue_id_to_device[sensor_info.power_id] = device + hue_id_to_device[sensor_info.temperature_id] = device + hue_id_to_device[sensor_info.light_level_id] = device device:set_field(Fields.DEVICE_TYPE, HueDeviceTypes.MOTION, { persist = true }) device:set_field(Fields.HUE_DEVICE_ID, sensor_info.hue_device_id, { persist = true }) @@ -62,7 +63,7 @@ function MotionLifecycleHandlers.added(driver, device, parent_device_id, resourc device:set_field(Fields._ADDED, true, { persist = true }) device:set_field(Fields._REFRESH_AFTER_INIT, true, { persist = true }) - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + hue_id_to_device[device_sensor_resource_id] = device end ---@param driver HueDriver @@ -78,8 +79,9 @@ function MotionLifecycleHandlers.init(driver, device) log.debug("resource id " .. tostring(device_sensor_resource_id)) local hue_device_id = device:get_field(Fields.HUE_DEVICE_ID) - if not driver.hue_identifier_to_device_record[device_sensor_resource_id] then - driver.hue_identifier_to_device_record[device_sensor_resource_id] = device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) or {} + if not hue_id_to_device[device_sensor_resource_id] then + hue_id_to_device[device_sensor_resource_id] = device end local sensor_info, err sensor_info = Discovery.device_state_disco_cache[device_sensor_resource_id] diff --git a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua index 1cda7fb921..3ffd02b3b6 100644 --- a/drivers/SmartThings/philips-hue/src/hue_driver_template.lua +++ b/drivers/SmartThings/philips-hue/src/hue_driver_template.lua @@ -70,7 +70,7 @@ local set_color_temp_handler = utils.safe_wrap_handler(command_handlers.set_colo --- @class HueDriver:Driver --- @field public ignored_bridges table --- @field public joined_bridges table ---- @field public hue_identifier_to_device_record table +--- @field public hue_identifier_to_device_record_by_bridge table> --- @field public services_for_device_rid table> Map the device resource ID to another map that goes from service rid to service rtype --- @field public waiting_grandchildren table? --- @field public stray_device_tx table cosock channel @@ -113,7 +113,7 @@ function HueDriver.new_driver_template(dbg_config) ignored_bridges = {}, joined_bridges = {}, - hue_identifier_to_device_record = {}, + hue_identifier_to_device_record_by_bridge = {}, services_for_device_rid = {}, -- the only real way we have to know which bridge a bulb wants to use at migration time -- is by looking at the stored api key so we will make a map to look up bridge IDs with diff --git a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua index d1f1cd6753..d3fb4813c9 100644 --- a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua @@ -1,6 +1,7 @@ local log = require "log" local Fields = require "fields" local cosock = require "cosock" +local utils = require "utils" --- Room or zone with the children translated from their hue device id or light resource id to --- their SmartThings represented device object. The grouped light resource id is also moved into @@ -100,10 +101,11 @@ end ---@param group_kind string room or zone ---@param driver HueDriver ---@param hue_id_to_device table +---@param light_id_to_device table ---@param resp table? ---@param err any? ---@return HueLightGroup[]? -local function handle_group_scan_response(group_kind, driver, hue_id_to_device, resp, err) +local function handle_group_scan_response(group_kind, driver, hue_id_to_device, light_id_to_device, resp, err) if err or not resp then log.error(string.format("Failed to scan for %s: %s", group_kind, err or "unknown error")) return nil @@ -121,7 +123,7 @@ local function handle_group_scan_response(group_kind, driver, hue_id_to_device, log.info(string.format("Successfully got %d %s", #resp.data, group_kind)) for _, group in ipairs(resp.data) do - build_hue_light_group(group, hue_id_to_device, driver.hue_identifier_to_device_record) + build_hue_light_group(group, hue_id_to_device, light_id_to_device) log.info(string.format("Found light group %s with %d device records", group.id, #group.devices)) end @@ -134,12 +136,14 @@ end --- @param hue_id_to_device table function grouped_utils.scan_groups(driver, bridge_device, api, hue_id_to_device) local rooms, zones - while not (rooms and zones) do -- TODO: Should this be one and done? Timeout? + -- These are the hue light/other service ids rather than the hue device ids + local light_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} + while not (rooms and zones) do if not rooms then - rooms = handle_group_scan_response("rooms", driver, hue_id_to_device, api:get_rooms()) + rooms = handle_group_scan_response("rooms", driver, hue_id_to_device, light_id_to_device, api:get_rooms()) end if not zones then - zones = handle_group_scan_response("zones", driver, hue_id_to_device, api:get_zones()) + zones = handle_group_scan_response("zones", driver, hue_id_to_device, light_id_to_device, api:get_zones()) end end -- Combine rooms and zones. @@ -204,7 +208,10 @@ function grouped_utils.group_update(driver, bridge_device, to_update) if to_update.children then local devices = build_group_device_table( - to_update, build_hue_id_to_device_map(bridge_device), driver.hue_identifier_to_device_record + to_update, + build_hue_id_to_device_map(bridge_device), + -- These are the hue light/other service ids rather than the hue device ids + utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} ) -- check if number of children has changed and if we need to move it update_index = #devices ~= #group.devices @@ -241,7 +248,10 @@ function grouped_utils.group_add(driver, bridge_device, to_add) end local hue_light_group = build_hue_light_group( - to_add, build_hue_id_to_device_map(bridge_device), driver.hue_identifier_to_device_record + to_add, + build_hue_id_to_device_map(bridge_device), + -- These are the hue light/other service ids rather than the hue device ids + utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} ) insert(groups, hue_light_group) log.info(string.format("Adding group %s, %d devices", diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua index 53d2cd2698..05905b80ff 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_bridge_utils.lua @@ -40,6 +40,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u { [HueApi.APPLICATION_KEY_HEADER] = api_key }, nil ) + local hue_identifier_to_device_record = utils.get_hue_id_to_device_table_by_bridge(driver, bridge_device) or {} eventsource.onopen = function(msg) log.info_with({ hub_logs = true }, @@ -171,7 +172,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u local resource_ids = {} if update_data.type == "zigbee_connectivity" and update_data.owner ~= nil then for rid, rtype in pairs(driver.services_for_device_rid[update_data.owner.rid] or {}) do - if driver.hue_identifier_to_device_record[rid] then + if hue_identifier_to_device_record[rid] then log.debug(string.format("Adding RID %s to resource_ids", rid)) table.insert(resource_ids, rid) end @@ -186,7 +187,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u end for _, resource_id in ipairs(resource_ids) do log.debug(string.format("Looking for device record for %s", resource_id)) - local child_device = driver.hue_identifier_to_device_record[resource_id] + local child_device = hue_identifier_to_device_record[resource_id] if child_device ~= nil and child_device.id ~= nil then if update_data.type == "zigbee_connectivity" then log.debug("emitting event for zigbee connectivity") @@ -203,7 +204,7 @@ function hue_bridge_utils.do_bridge_network_init(driver, bridge_device, bridge_u for _, delete_data in ipairs(event.data) do if HueDeviceTypes.can_join_device_for_service(delete_data.type) then local resource_id = delete_data.id - local child_device = driver.hue_identifier_to_device_record[resource_id] + local child_device = hue_identifier_to_device_record[resource_id] if child_device ~= nil and child_device.id ~= nil then log.info( string.format( diff --git a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua index a8df7520d6..e9212b2433 100644 --- a/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua +++ b/drivers/SmartThings/philips-hue/src/utils/hue_multi_service_device_utils/sensor.lua @@ -1,3 +1,5 @@ +local utils = require "utils" + local SensorMultiServiceHelper = {} function SensorMultiServiceHelper.update_multi_service_device_maps(driver, device, hue_device_id, sensor_info) local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} @@ -15,8 +17,9 @@ function SensorMultiServiceHelper.update_multi_service_device_maps(driver, devic end driver.services_for_device_rid[hue_device_id] = svc_rids_for_device + local hue_id_to_device = utils.get_hue_id_to_device_table_by_bridge(driver, device) for rid, _ in pairs(driver.services_for_device_rid[hue_device_id]) do - driver.hue_identifier_to_device_record[rid] = device + hue_id_to_device[rid] = device end end diff --git a/drivers/SmartThings/philips-hue/src/utils/init.lua b/drivers/SmartThings/philips-hue/src/utils/init.lua index 0819286ab5..3f65c9a4d5 100644 --- a/drivers/SmartThings/philips-hue/src/utils/init.lua +++ b/drivers/SmartThings/philips-hue/src/utils/init.lua @@ -345,6 +345,7 @@ end ---@param driver HueDriver ---@param device HueDevice ---@param parent_device_id string? +---@param quiet boolean? ---@return HueBridgeDevice? bridge_device function utils.get_hue_bridge_for_device(driver, device, parent_device_id, quiet) local _ = quiet or @@ -371,6 +372,27 @@ function utils.get_hue_bridge_for_device(driver, device, parent_device_id, quiet return utils.get_hue_bridge_for_device(driver, parent_device, nil, quiet) end +--- Get the mapping of hue id to device table by associated bridge. The mapping is separated by bridge to account +--- for devices migrated to a new hue bridge. +---@param driver HueDriver +---@param bridge_or_device HueDevice +---@return table? hue_id_to_device +function utils.get_hue_id_to_device_table_by_bridge(driver, bridge_or_device) + -- If bridge_or_device is a bridge this will just return itself + local bridge = utils.get_hue_bridge_for_device(driver, bridge_or_device) + if not bridge then + log.warn(string.format("Failed to lookup bridge for device %s", bridge_or_device.label)) + return nil + end + local bridge_id = bridge:get_field(Fields.BRIDGE_ID) + if not bridge_id then + log.warn(string.format("Failed to get bridge id for %s", bridge.label)) + return nil + end + driver.hue_identifier_to_device_record_by_bridge[bridge_id] = driver.hue_identifier_to_device_record_by_bridge[bridge_id] or {} + return driver.hue_identifier_to_device_record_by_bridge[bridge_id] +end + --- build a exponential backoff time value generator --- ---@param max number the maximum wait interval (not including `rand factor`) From d0455c74ecbff81280f7ad5bd9884e18a95dd341 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 10 Sep 2025 15:08:30 -0500 Subject: [PATCH 109/449] add new base rain, leak, and freeze profiles (#2384) --- .../matter-sensor/profiles/freeze.yml | 18 ++++++++++++++++++ .../matter-sensor/profiles/leak.yml | 12 ++++++++++++ .../matter-sensor/profiles/rain.yml | 12 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 drivers/SmartThings/matter-sensor/profiles/freeze.yml create mode 100644 drivers/SmartThings/matter-sensor/profiles/leak.yml create mode 100644 drivers/SmartThings/matter-sensor/profiles/rain.yml diff --git a/drivers/SmartThings/matter-sensor/profiles/freeze.yml b/drivers/SmartThings/matter-sensor/profiles/freeze.yml new file mode 100644 index 0000000000..111fc4c71a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/freeze.yml @@ -0,0 +1,18 @@ +name: freeze +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - cleared + - freeze + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterFreezeDetector diff --git a/drivers/SmartThings/matter-sensor/profiles/leak.yml b/drivers/SmartThings/matter-sensor/profiles/leak.yml new file mode 100644 index 0000000000..a2f6d07727 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/leak.yml @@ -0,0 +1,12 @@ +name: leak +components: +- id: main + capabilities: + - id: waterSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: LeakSensor diff --git a/drivers/SmartThings/matter-sensor/profiles/rain.yml b/drivers/SmartThings/matter-sensor/profiles/rain.yml new file mode 100644 index 0000000000..c55b5d489f --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/rain.yml @@ -0,0 +1,12 @@ +name: rain +components: +- id: main + capabilities: + - id: rainSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RainSensor From a5eb62ccc1988db6aee6b955a4b849bbdc9662b1 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Mon, 28 Jul 2025 13:27:36 -0500 Subject: [PATCH 110/449] fix(sonos): Improved handling of bonded set membership changes. We have historically ignored non-primary devices in bonded sets during discovery/onboarding, going back to the pre-Edge days. Bonded sets are things like stereo pairs or home theater setups. These devices cannot be controlled via the WSS LAN API at all, and they aren't treated as being part of a Group in the Sonos group model; the intent is for API consumers to treat the entire bonded set as a single "player". Only one of the devices in the set exposes an API endpoint, which differs from a Group where all the players expose an endpoint. While groups service most commands via calling the API endpoint on the Group Coordinator, certain commands like volume can be sent to non-coordinator players. This is not the case with bonded sets; the entire set always acts atomically, and only the primary device in the set can service commands. Hence, we treat non-primary devices as not controllable in a bonded set, and we don't onboard them. However, we didn't really have very robust handling of devices that become part of a bonded set at runtime after onboarding. This led to unnecessary CPU and RAM usage by creating a few tight loops due to unexpected failure/edge cases when making certain calls. Because we would fail to discover this device under normal circumstances, the preferred way to handle this transition is to mark the device as being offline. We discussed emitting a delete for these devices, but it seems that it wouldn't be too uncommon for some users to create and destroy bonded sets ephemerally the way one might do with a Group. For this reason we decided we would avoid deleting the records. If a non-primary in a bonded set is removed from the bonded set, it will be marked as online again. --- .../sonos/src/api/event_handlers.lua | 49 ++++--- .../sonos/src/api/sonos_connection.lua | 46 ++++-- .../sonos/src/api/sonos_ssdp_discovery.lua | 2 +- drivers/SmartThings/sonos/src/fields.lua | 1 + drivers/SmartThings/sonos/src/init.lua | 4 +- .../sonos/src/lifecycle_handlers.lua | 10 +- .../SmartThings/sonos/src/sonos_driver.lua | 57 ++++++-- drivers/SmartThings/sonos/src/sonos_state.lua | 137 +++++++++++++----- drivers/SmartThings/sonos/src/types.lua | 5 +- drivers/SmartThings/sonos/src/utils.lua | 8 +- 10 files changed, 219 insertions(+), 100 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/event_handlers.lua b/drivers/SmartThings/sonos/src/api/event_handlers.lua index a4fe85766e..ad9b697740 100644 --- a/drivers/SmartThings/sonos/src/api/event_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/event_handlers.lua @@ -1,8 +1,12 @@ local capabilities = require "st.capabilities" +local swGenCapability = capabilities["stus.softwareGeneration"] + local log = require "log" local st_utils = require "st.utils" +local PlayerFields = require "fields".SonosPlayerFields + local CapEventHandlers = {} CapEventHandlers.PlaybackStatus = { @@ -12,41 +16,52 @@ CapEventHandlers.PlaybackStatus = { Playing = "PLAYBACK_STATE_PLAYING", } +local function _do_emit(device, attribute_event) + local bonded = device:get_field(PlayerFields.BONDED) + if not bonded then + device:emit_event(attribute_event) + end +end + +function CapEventHandlers.handle_sw_gen(device, sw_gen) + _do_emit(device, swGenCapability.generation(string.format("%s", sw_gen))) +end + function CapEventHandlers.handle_player_volume(device, new_volume, is_muted) - device:emit_event(capabilities.audioVolume.volume(new_volume)) + _do_emit(device, capabilities.audioVolume.volume(new_volume)) if is_muted then - device:emit_event(capabilities.audioMute.mute.muted()) + _do_emit(device, capabilities.audioMute.mute.muted()) else - device:emit_event(capabilities.audioMute.mute.unmuted()) + _do_emit(device, capabilities.audioMute.mute.unmuted()) end end function CapEventHandlers.handle_group_volume(device, new_volume, is_muted) - device:emit_event(capabilities.mediaGroup.groupVolume(new_volume)) + _do_emit(device, capabilities.mediaGroup.groupVolume(new_volume)) if is_muted then - device:emit_event(capabilities.mediaGroup.groupMute.muted()) + _do_emit(device, capabilities.mediaGroup.groupMute.muted()) else - device:emit_event(capabilities.mediaGroup.groupMute.unmuted()) + _do_emit(device, capabilities.mediaGroup.groupMute.unmuted()) end end function CapEventHandlers.handle_group_role_update(device, group_role) - device:emit_event(capabilities.mediaGroup.groupRole(group_role)) + _do_emit(device, capabilities.mediaGroup.groupRole(group_role)) end function CapEventHandlers.handle_group_coordinator_update(device, coordinator_id) - device:emit_event(capabilities.mediaGroup.groupPrimaryDeviceId(coordinator_id)) + _do_emit(device, capabilities.mediaGroup.groupPrimaryDeviceId(coordinator_id)) end function CapEventHandlers.handle_group_id_update(device, group_id) - device:emit_event(capabilities.mediaGroup.groupId(group_id)) + _do_emit(device, capabilities.mediaGroup.groupId(group_id)) end function CapEventHandlers.handle_group_update(device, group_info) local groupRole, groupPrimaryDeviceId, groupId = table.unpack(group_info) - device:emit_event(capabilities.mediaGroup.groupRole(groupRole)) - device:emit_event(capabilities.mediaGroup.groupPrimaryDeviceId(groupPrimaryDeviceId)) - device:emit_event(capabilities.mediaGroup.groupId(groupId)) + _do_emit(device, capabilities.mediaGroup.groupRole(groupRole)) + _do_emit(device, capabilities.mediaGroup.groupPrimaryDeviceId(groupPrimaryDeviceId)) + _do_emit(device, capabilities.mediaGroup.groupId(groupId)) end function CapEventHandlers.handle_audio_clip_status(device, clips) @@ -61,11 +76,11 @@ end function CapEventHandlers.handle_playback_status(device, playback_state) if playback_state == CapEventHandlers.PlaybackStatus.Playing then - device:emit_event(capabilities.mediaPlayback.playbackStatus.playing()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.playing()) elseif playback_state == CapEventHandlers.PlaybackStatus.Idle then - device:emit_event(capabilities.mediaPlayback.playbackStatus.stopped()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.stopped()) elseif playback_state == CapEventHandlers.PlaybackStatus.Paused then - device:emit_event(capabilities.mediaPlayback.playbackStatus.paused()) + _do_emit(device, capabilities.mediaPlayback.playbackStatus.paused()) elseif playback_state == CapEventHandlers.PlaybackStatus.Buffering then -- TODO the DTH doesn't currently do anything w/ buffering; -- might be worth figuring out what to do with this in the future. @@ -74,7 +89,7 @@ function CapEventHandlers.handle_playback_status(device, playback_state) end function CapEventHandlers.update_favorites(device, new_favorites) - device:emit_event(capabilities.mediaPresets.presets(new_favorites)) + _do_emit(device, capabilities.mediaPresets.presets(new_favorites)) end function CapEventHandlers.handle_playback_metadata_update(device, metadata_status_body) @@ -128,7 +143,7 @@ function CapEventHandlers.handle_playback_metadata_update(device, metadata_statu end if type(audio_track_data.title) == "string" then - device:emit_event(capabilities.audioTrackData.audioTrackData(audio_track_data)) + _do_emit(device, capabilities.audioTrackData.audioTrackData(audio_track_data)) end end diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index f1773e24aa..d854401313 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -166,8 +166,12 @@ local function _open_coordinator_socket(sonos_conn, household_id, self_player_id return end - _, err = - Router.open_socket_for_player(household_id, coordinator_id, coordinator.websocketUrl, api_key) + _, err = Router.open_socket_for_player( + household_id, + coordinator_id, + coordinator.player.websocketUrl, + api_key + ) if err ~= nil then log.error( string.format( @@ -302,10 +306,13 @@ end --- @return SonosConnection function SonosConnection.new(driver, device) log.debug(string.format("Creating new SonosConnection for %s", device.label)) - local self = setmetatable( - { driver = driver, device = device, _listener_uuids = {}, _initialized = false, _reconnecting = false }, - SonosConnection - ) + local self = setmetatable({ + driver = driver, + device = device, + _listener_uuids = {}, + _initialized = false, + _reconnecting = false, + }, SonosConnection) -- capture the label here in case something goes wonky like a callback being fired after a -- device is removed @@ -358,19 +365,28 @@ function SonosConnection.new(driver, device) local household_id, current_coordinator = self.driver.sonos:get_coordinator_for_device(self.device) local _, player_id = self.driver.sonos:get_player_for_device(self.device) - self.driver.sonos:update_household_info(header.householdId, body, self.device) + self.driver.sonos:update_household_info(header.householdId, body, self.driver) self.driver.sonos:update_device_record_from_state(header.householdId, self.device) local _, updated_coordinator = self.driver.sonos:get_coordinator_for_device(self.device) + local bonded = self.device:get_field(PlayerFields.BONDED) + if bonded then + self:stop() + end + Router.cleanup_unused_sockets(self.driver) - if not self:coordinator_running() then - --TODO this is not infallible - _open_coordinator_socket(self, household_id, player_id) - end + if not bonded then + if not self:coordinator_running() then + --TODO this is not infallible + _open_coordinator_socket(self, household_id, player_id) + end - if current_coordinator ~= updated_coordinator then - self:refresh_subscriptions() + if current_coordinator ~= updated_coordinator then + self:refresh_subscriptions() + end + else + self.device:offline() end elseif header.type == "playerVolume" then log.trace(string.format("PlayerVolume type message for %s", device_name)) @@ -477,7 +493,7 @@ function SonosConnection.new(driver, device) return end - local url_ip = lb_utils.force_url_table(coordinator_player.websocketUrl).host + local url_ip = lb_utils.force_url_table(coordinator_player.player.websocketUrl).host local base_url = lb_utils.force_url_table( string.format("https://%s:%s", url_ip, SonosApi.DEFAULT_SONOS_PORT) ) @@ -590,7 +606,7 @@ function SonosConnection:coordinator_running() ) ) end - return type(unique_key) == "string" and Router.is_connected(unique_key) and self._initialized + return type(unique_key) == "string" and Router.is_connected(unique_key) end function SonosConnection:refresh_subscriptions(maybe_reply_tx) diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index 88e0cddbd9..be3ae6092a 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -300,7 +300,7 @@ function sonos_ssdp.spawn_persistent_ssdp_task() if info_to_send then if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then log.error_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") ) return diff --git a/drivers/SmartThings/sonos/src/fields.lua b/drivers/SmartThings/sonos/src/fields.lua index 6a219ef452..11b658a835 100644 --- a/drivers/SmartThings/sonos/src/fields.lua +++ b/drivers/SmartThings/sonos/src/fields.lua @@ -5,6 +5,7 @@ local Fields = {} Fields.SonosPlayerFields = { _IS_INIT = "init", _IS_SCANNING = "scanning", + BONDED = "bonded", CONNECTION = "conn", UNIQUE_KEY = "unique_key", HOUSEHOLD_ID = "householdId", diff --git a/drivers/SmartThings/sonos/src/init.lua b/drivers/SmartThings/sonos/src/init.lua index bfe1c0eb9e..db4e5f6013 100644 --- a/drivers/SmartThings/sonos/src/init.lua +++ b/drivers/SmartThings/sonos/src/init.lua @@ -39,6 +39,6 @@ if api_version < 14 then driver:start_ssdp_event_task() end -log.info "Starting Sonos run loop" +log.info("Starting Sonos run loop") driver:run() -log.info "Exiting Sonos run loop" +log.info("Exiting Sonos run loop") diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 747381f798..c62b90abcd 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -164,10 +164,12 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) return end log.error_with( - { hub_logs = true }, - "Error handling Sonos player initialization: %s, error code: %s", - error, - (error_code or "N/A") + { hub_logs = false }, + string.format( + "Error handling Sonos player initialization: %s, error code: %s", + error, + (error_code or "N/A") + ) ) end end diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index c603a7f399..624a93ccf8 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -42,11 +42,27 @@ local ONE_HOUR_IN_SECONDS = 3600 ---@field private waiting_for_oauth_token boolean ---@field private startup_state_received boolean ---@field private devices_waiting_for_startup_state SonosDevice[] +---@field package bonded_devices table map of Device device_network_id to a boolean indicating if the device is currently known as a bonded device. --- ---@field public ssdp_task SonosPersistentSsdpTask? ---@field private ssdp_event_thread_handle table? local SonosDriver = {} +---@param device SonosDevice +function SonosDriver:update_bonded_device_tracking(device) + local already_bonded = self.bonded_devices[device.device_network_id] + local currently_bonded = device:get_field(PlayerFields.BONDED) + self.bonded_devices[device.device_network_id] = currently_bonded + + if currently_bonded and not already_bonded then + device:offline() + end + + if already_bonded and not currently_bonded then + SonosDriverLifecycleHandlers.initialize_device(self, device) + end +end + function SonosDriver:has_received_startup_state() return self.startup_state_received end @@ -127,13 +143,13 @@ end function SonosDriver:handle_augmented_store_delete(update_key) if update_key == "endpointAppInfo" then if update_key == "endpointAppInfo" then - log.trace "deleting endpoint app info" + log.trace("deleting endpoint app info") self.oauth.endpoint_app_info = nil elseif update_key == "sonosOAuthToken" then - log.trace "deleting OAuth Token" + log.trace("deleting OAuth Token") self.oauth.token = nil elseif update_key == "force_oauth" then - log.trace "deleting Force OAuth" + log.trace("deleting Force OAuth") self.oauth.force_oauth = nil else log.debug(string.format("received delete of unexpected key: %s", update_key)) @@ -423,9 +439,15 @@ local function make_ssdp_event_handler( local event, recv_err = discovery_event_subscription:receive() if event then + local mac_addr = utils.extract_mac_addr(event.discovery_info.device) local unique_key = utils.sonos_unique_key_from_ssdp(event.ssdp_info) if - event.force_refresh or not (unauthorized[unique_key] or discovered[unique_key]) + event.force_refresh + or not ( + unauthorized[unique_key] + or discovered[unique_key] + or driver.bonded_devices[mac_addr] + ) then local _, api_key = driver:check_auth(event) local success, handle_err, err_code = @@ -435,7 +457,7 @@ local function make_ssdp_event_handler( unauthorized[unique_key] = event end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format("Failed to handle discovered speaker: %s", handle_err) ) else @@ -482,13 +504,17 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) -- If the SSDP Group Info is an empty string, then that means it's the non-primary -- speaker in a bonded set (e.g. a home theater system, a stereo pair, etc). -- These aren't the same as speaker groups, and bonded speakers can't be controlled - -- via websocket at all. So we ignore all bonded non-primary speakers - if #info.ssdp_info.group_id == 0 then - return nil, - string.format( - "Player %s is a non-primary bonded Sonos device, ignoring", - info.discovery_info.device.name - ) + -- via websocket at all. So we ignore all bonded non-primary speakers if they are not + -- already onboarded. + + local discovery_info_mac_addr = utils.extract_mac_addr(info.discovery_info.device) + local bonded = (#info.ssdp_info.group_id == 0) + self.bonded_devices[discovery_info_mac_addr] = bonded + + local maybe_device = self:get_device_by_dni(discovery_info_mac_addr) + if maybe_device then + maybe_device:set_field(PlayerFields.BONDED, bonded, { persist = false }) + self:update_bonded_device_tracking(maybe_device) end api_key = api_key or self:get_fallback_api_key() @@ -543,7 +569,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end --- @cast response SonosGroupsResponseBody - self.sonos:update_household_info(info.ssdp_info.household_id, response) + self.sonos:update_household_info(info.ssdp_info.household_id, response, self) local device_to_update, device_mac_addr @@ -565,7 +591,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) if not (info and info.discovery_info and info.discovery_info.device) then return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") end - device_mac_addr = utils.extract_mac_addr(info.discovery_info.device) + device_mac_addr = discovery_info_mac_addr end if not device_to_update then @@ -578,7 +604,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) if device_to_update then self.dni_to_device_id[device_mac_addr] = device_to_update.id self.sonos:associate_device_record(device_to_update, info) - else + elseif not bonded then local name = info.discovery_info.device.name or info.discovery_info.device.modelDisplayName or "Unknown Sonos Player" @@ -631,6 +657,7 @@ function SonosDriver.new_driver_template() waiting_for_oauth_token = false, startup_state_received = false, devices_waiting_for_startup_state = {}, + bonded_devices = utils.new_mac_address_keyed_table(), dni_to_device_id = utils.new_mac_address_keyed_table(), lifecycle_handlers = SonosDriverLifecycleHandlers, capability_handlers = { diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index 25c9e47b2b..fb316f8625 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -1,7 +1,5 @@ -local capabilities = require "st.capabilities" local log = require "log" local st_utils = require "st.utils" -local swGenCapability = capabilities["stus.softwareGeneration"] local utils = require "utils" @@ -13,7 +11,8 @@ local SonosConnection = require "api.sonos_connection" --- Information on an entire Sonos system ("household"), such as its current groups, list of players, etc. --- @field public id HouseholdId --- @field public groups table All of the current groups in the system ---- @field public players table All of the current players in the system +--- @field public players table All of the current players in the system +--- @field public bonded_players table PlayerID's in this map that map to true are non-primary bonded players, and not controllable. --- @field public player_to_group table quick lookup from Player ID -> Group ID --- @field public st_devices table Player ID -> ST Device Record UUID information for the household --- @field public favorites SonosFavorites all of the favorites/presets in the system @@ -23,6 +22,11 @@ function _household_mt:reset() self.groups = utils.new_case_insensitive_table() self.players = utils.new_case_insensitive_table() self.player_to_group = utils.new_case_insensitive_table() + -- previously bonded devices should not be un-bonded after a reset since these should + -- not be treated as distinct devices + if not self.bonded_players then + self.bonded_players = utils.new_case_insensitive_table() + end end _household_mt.__index = _household_mt @@ -46,10 +50,10 @@ local function make_households_table() local households_table_inner = utils.new_case_insensitive_table() local households_table = setmetatable({}, { - __index = function(tbl, key) + __index = function(_, key) return households_table_inner[key] end, - __newindex = function(tbl, key, value) + __newindex = function(_, key, value) households_table_inner[key] = value end, __metatable = "SonosHouseholds", @@ -73,7 +77,7 @@ end local _STATE = { ---@type Households households = make_households_table(), - ---@type table + ---@type table device_record_map = {}, } @@ -100,8 +104,11 @@ function SonosState:associate_device_record(device, info) return end - local group = household.groups[group_id] + if not group_id or #group_id == 0 then + group_id = household.player_to_group[player_id or ""] or "" + end + local group = household.groups[group_id] if not group then log.error( string.format( @@ -112,9 +119,10 @@ function SonosState:associate_device_record(device, info) return end - local player = household.players[player_id] + local player = (household.players[player_id] or {}).player + local sonos_device = (household.players[player_id] or {}).device - if not player then + if not (player and sonos_device) then log.error( string.format( "No record of Sonos player for device %s", @@ -124,15 +132,24 @@ function SonosState:associate_device_record(device, info) return end - household.st_devices[player.id] = device.id + household.st_devices[sonos_device.id] = device.id - _STATE.device_record_map[device.id] = { group = group, player = player, household = household } + _STATE.device_record_map[device.id] = + { sonos_device = sonos_device, group = group, player = player, household = household } - device:set_field(PlayerFields.SW_GEN, info.discovery_info.device.swGen, { persist = true }) - device:emit_event( - swGenCapability.generation(string.format("%s", info.discovery_info.device.swGen)) + local bonded = household.bonded_players[sonos_device.id or {}] and true or false + + local sw_gen_changed = utils.update_field_if_changed( + device, + PlayerFields.SW_GEN, + info.discovery_info.device.swGen, + { persist = true } ) + if sw_gen_changed then + CapEventHandlers.handle_sw_gen(device, info.discovery_info.device.swGen) + end + device:set_field(PlayerFields.REST_URL, info.discovery_info.restUrl, { persist = true }) local sonos_conn = device:get_field(PlayerFields.CONNECTION) @@ -144,7 +161,9 @@ function SonosState:associate_device_record(device, info) { persist = true } ) - if websocket_url_changed and connected then + local should_stop_conn = connected and (bonded or websocket_url_changed) + + if should_stop_conn then sonos_conn:stop() sonos_conn = nil device:set_field(PlayerFields.CONNECTION, nil) @@ -157,13 +176,17 @@ function SonosState:associate_device_record(device, info) { persist = true } ) - local player_id_changed = - utils.update_field_if_changed(device, PlayerFields.PLAYER_ID, player.id, { persist = true }) + local player_id_changed = utils.update_field_if_changed( + device, + PlayerFields.PLAYER_ID, + sonos_device.id, + { persist = true } + ) local need_refresh = connected and (websocket_url_changed or household_id_changed or player_id_changed) - if sonos_conn == nil then + if not bonded and sonos_conn == nil then sonos_conn = SonosConnection.new(device.driver, device) device:set_field(PlayerFields.CONNECTION, sonos_conn) sonos_conn:start() @@ -175,6 +198,11 @@ function SonosState:associate_device_record(device, info) end self:update_device_record_group_info(household, group, device) + + -- device can't be controlled, mark the device as being offline. + if bonded then + device:offline() + end end ---@param household SonosHousehold @@ -182,8 +210,11 @@ end ---@param device SonosDevice function SonosState:update_device_record_group_info(household, group, device) local player_id = device:get_field(PlayerFields.PLAYER_ID) + local bonded = ((household or {}).bonded_players or {})[player_id] and true or false local group_role - if + if bonded then + group_role = "auxilary" + elseif ( type(household) == "table" and type(household.groups) == "table" @@ -205,13 +236,13 @@ function SonosState:update_device_record_group_info(household, group, device) local field_changed = utils.update_field_if_changed(device, PlayerFields.GROUP_ID, group.id, { persist = true }) - if field_changed then + if not bonded and field_changed then CapEventHandlers.handle_group_id_update(device, group.id) end field_changed = utils.update_field_if_changed(device, PlayerFields.GROUP_ROLE, group_role, { persist = true }) - if field_changed then + if not bonded and field_changed then CapEventHandlers.handle_group_role_update(device, group_role) end @@ -221,9 +252,13 @@ function SonosState:update_device_record_group_info(household, group, device) group.coordinatorId, { persist = true } ) - if field_changed then + if not bonded and field_changed then CapEventHandlers.handle_group_coordinator_update(device, group.coordinatorId) end + + if bonded then + device:offline() + end end function SonosState:remove_device_record_association(device) @@ -273,8 +308,10 @@ end --- @param id HouseholdId --- @param groups_event SonosGroupsResponseBody -function SonosState:update_household_info(id, groups_event) +--- @param driver SonosDriver +function SonosState:update_household_info(id, groups_event, driver) local household = _STATE.households:get_or_init(id) + local known_bonded_players = household.bonded_players or {} household:reset() local groups, players = groups_event.groups, groups_event.players @@ -283,25 +320,45 @@ function SonosState:update_household_info(id, groups_event) household.groups[group.id] = group for _, playerId in ipairs(group.playerIds) do household.player_to_group[playerId] = group.id + end + end + + for _, player in ipairs(players) do + for _, device in ipairs(player.devices) do + household.players[device.id] = { player = player, device = device } + local previously_bonded = known_bonded_players[device.id] and true or false + local currently_bonded + local group_id + -- non-primary bonded players are excluded from a group's list of PlayerID's so we use the group membership + -- of the primary device + if type(device.primaryDeviceId) == "string" and device.primaryDeviceId ~= "" then + currently_bonded = true + group_id = household.player_to_group[device.primaryDeviceId] + else + currently_bonded = false + group_id = household.player_to_group[device.id] + end + household.player_to_group[device.id] = group_id + household.bonded_players[device.id] = currently_bonded - local maybe_device_id = household.st_devices[playerId] + local maybe_device_id = household.st_devices[device.id] if maybe_device_id then _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} - _STATE.device_record_map[maybe_device_id].group = group _STATE.device_record_map[maybe_device_id].household = household + _STATE.device_record_map[maybe_device_id].group = household.groups[group_id] + _STATE.device_record_map[maybe_device_id].player = player + _STATE.device_record_map[maybe_device_id].sonos_device = device + if previously_bonded ~= currently_bonded then + local target_device = driver:get_device_info(maybe_device_id) + if target_device then + target_device:set_field(PlayerFields.BONDED, currently_bonded, { persist = false }) + driver:update_bonded_device_tracking(target_device) + end + end end end end - for _, player in ipairs(players) do - household.players[player.id] = player - local maybe_device_id = household.st_devices[player.id] - if maybe_device_id then - _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} - _STATE.device_record_map[maybe_device_id].player = player - end - end - household.id = id _STATE.households[id] = household end @@ -312,7 +369,7 @@ end --- @return string? error nil on success function SonosState:get_group_for_player(household_id, player_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, player_id = player_id }, "Get Group For Player", @@ -339,7 +396,7 @@ end --- @return PlayerId?,string? function SonosState:get_coordinator_for_player(household_id, player_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, player_id = player_id }, "Get Coordinator For Player", @@ -361,7 +418,7 @@ end --- @return PlayerId?,string? function SonosState:get_coordinator_for_group(household_id, group_id) log.debug_with( - { hub_logs = true }, + { hub_logs = false }, st_utils.stringify_table( { household_id = household_id, group_id = group_id }, "Get Coordinator For Group", @@ -432,7 +489,7 @@ function SonosState:get_sonos_ids_for_device(device) -- player id *should* be stable if not player_id then - player_id = sonos_objects.player.id + player_id = sonos_objects.sonos_device.id device:set_field(PlayerFields.PLAYER_ID, player_id, { persist = true }) end @@ -464,7 +521,7 @@ end --- @return nil|string error nil on success function SonosState:get_group_for_device(device) if type(device) ~= "table" then - return nil, string.format("Invalid device argument for get_player_for_device: %s", device) + return nil, string.format("Invalid device argument for get_group_for_device: %s", device) end local household_id, group_id, _, err = self:get_sonos_ids_for_device(device) if err then @@ -479,7 +536,7 @@ end --- @return nil|string error nil on success function SonosState:get_coordinator_for_device(device) if type(device) ~= "table" then - return nil, string.format("Invalid device argument for get_player_for_device: %s", device) + return nil, string.format("Invalid device argument for get_coordinator_for_device: %s", device) end local household_id, group_id, _, err = self:get_sonos_ids_for_device(device) if err then diff --git a/drivers/SmartThings/sonos/src/types.lua b/drivers/SmartThings/sonos/src/types.lua index 65bcb3041f..a1be63fe3f 100644 --- a/drivers/SmartThings/sonos/src/types.lua +++ b/drivers/SmartThings/sonos/src/types.lua @@ -28,7 +28,7 @@ ---@field public controlApi { [1]: string } --- Lua representation of the Sonos `deviceInfo` JSON Object: https://developer.sonos.com/build/control-sonos-players-lan/discover-lan/#deviceInfo-object ---- @class SonosDeviceInfo +--- @class SonosDeviceInfoObject --- @field public _objectType "deviceInfo" --- @field public id PlayerId The playerId. Also known as the deviceId. Used to address Sonos devices in the control API. --- @field public primaryDeviceId string Identifies the primary device in bonded sets. Primary devices leave the value blank, which omits the key from the message. The field is expected for secondary devices in stereo pairs and satellites in home theater configurations. @@ -49,7 +49,7 @@ --- Lua representation of the Sonos `discoveryInfo` JSON object: https://developer.sonos.com/build/control-sonos-players-lan/discover-lan/#discoveryInfo-object --- @class SonosDiscoveryInfo --- @field public _objectType "discoveryInfo" ---- @field public device SonosDeviceInfo The device object. This object presents immutable data that describes a Sonos device. Use this object to uniquely identify any Sonos device. See below for details. +--- @field public device SonosDeviceInfoObject The device object. This object presents immutable data that describes a Sonos device. Use this object to uniquely identify any Sonos device. See below for details. --- @field public householdId HouseholdId An opaque identifier assigned to the device during registration. This field may be missing prior to registration. --- @field public playerId PlayerId The identifier used to address this particular device in the control API. --- @field public groupId GroupId The currently assigned groupId, an ephemeral opaque identifier. This value is always correct, including for group members. @@ -141,6 +141,7 @@ --- @field public softwareVersion string --- @field public websocketUrl string --- @field public capabilities SonosCapabilities[] +--- @field public devices SonosDeviceInfoObject[] --- Sonos player local state --- @class PlayerDiscoveryState diff --git a/drivers/SmartThings/sonos/src/utils.lua b/drivers/SmartThings/sonos/src/utils.lua index 5ffb3a8a9d..1aa299b629 100644 --- a/drivers/SmartThings/sonos/src/utils.lua +++ b/drivers/SmartThings/sonos/src/utils.lua @@ -134,7 +134,7 @@ local function __case_insensitive_key_index(tbl, key) fmt_val = key or "" end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format( "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", fmt_val, @@ -157,7 +157,7 @@ local function __case_insensitive_key_newindex(tbl, key, value) fmt_val = key or "" end log.warn_with( - { hub_logs = true }, + { hub_logs = false }, string.format( "Expected `string` key for CaseInsensitiveKeyTable, received (%s: %s)", fmt_val, @@ -179,11 +179,11 @@ function utils.new_case_insensitive_table() return setmetatable({}, _case_insensitive_key_mt) end ----@param sonos_device_info SonosDeviceInfo +---@param sonos_device_info SonosDeviceInfoObject function utils.extract_mac_addr(sonos_device_info) if type(sonos_device_info) ~= "table" or type(sonos_device_info.serialNumber) ~= "string" then log.error_with( - { hub_logs = true }, + { hub_logs = false }, string.format("Bad sonos device info passed to `extract_mac_addr`: %s", sonos_device_info) ) end From 272189a77993fda4e5377b54bc80ad504f903efc Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 19 Aug 2025 10:34:31 -0500 Subject: [PATCH 111/449] sonos: Remove preemptive token refreshing. This does nothing without force refresh which could be problematic to use. --- drivers/SmartThings/sonos/src/sonos_driver.lua | 9 --------- 1 file changed, 9 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 624a93ccf8..fc018d510a 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -25,8 +25,6 @@ if not security_load_success then security = nil end -local ONE_HOUR_IN_SECONDS = 3600 - ---@class SonosDriver: Driver --- ---@field public datastore table driver persistent store @@ -384,13 +382,6 @@ function SonosDriver:get_oauth_token() local now = os.time() -- token has not expired yet if now < expiration then - -- token is expiring soon, so we pre-emptively refresh - if math.abs(expiration - now) < ONE_HOUR_IN_SECONDS then - local result, err = security.get_sonos_oauth() - if not result then - log.warn(string.format("Error requesting OAuth token via Security API: %s", err)) - end - end return self.oauth.token else return nil, "token expired" From 9a20e6a8f5abddb3e006459186cc9c74f04faa0a Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:43:27 -0500 Subject: [PATCH 112/449] sonos: Add bus to receive oauth endpoint app info events --- drivers/SmartThings/sonos/src/sonos_driver.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index fc018d510a..1451908bd1 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -36,8 +36,8 @@ end ---@field public sonos SonosState Local state related to the sonos systems ---@field public discovery fun(driver: SonosDriver, opts: table, should_continue: fun(): boolean) ---@field private oauth_token_bus cosock.Bus.Sender bus for broadcasting new oauth tokens that arrive on the environment channel +---@field private oauth_info_bus cosock.Bus.Sender bus for broadcasting new endpoint app info that arrives on the environment channel ---@field private oauth { token: {accessToken: string, expiresAt: number}, endpoint_app_info: { state: "connected"|"disconnected" }, force_oauth: boolean? } cached OAuth info ----@field private waiting_for_oauth_token boolean ---@field private startup_state_received boolean ---@field private devices_waiting_for_startup_state SonosDevice[] ---@field package bonded_devices table map of Device device_network_id to a boolean indicating if the device is currently known as a bonded device. @@ -103,9 +103,9 @@ end function SonosDriver:handle_augmented_data_change(update_key, decoded) if update_key == "endpointAppInfo" then self.oauth.endpoint_app_info = decoded + self.oauth_info_bus:send(decoded) elseif update_key == "sonosOAuthToken" then self.oauth.token = decoded - self.waiting_for_oauth_token = false self.oauth_token_bus:send(decoded) elseif update_key == "force_oauth" then self.oauth.force_oauth = decoded @@ -128,6 +128,15 @@ function SonosDriver:oauth_token_event_subscribe() return self.oauth_token_bus:subscribe() end +---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed +---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success +function SonosDriver:oauth_info_event_subscribe() + if api_version < 14 or security == nil then + return nil, "not supported" + end + return self.oauth_info_bus:subscribe() +end + function SonosDriver:update_after_startup_state_received() for k, v in pairs(self.hub_augmented_driver_data) do local decode_success, decoded = pcall(json.decode, v) @@ -143,9 +152,11 @@ function SonosDriver:handle_augmented_store_delete(update_key) if update_key == "endpointAppInfo" then log.trace("deleting endpoint app info") self.oauth.endpoint_app_info = nil + self.oauth_info_bus:send(nil) elseif update_key == "sonosOAuthToken" then log.trace("deleting OAuth Token") self.oauth.token = nil + self.oauth_token_bus:send(nil) elseif update_key == "force_oauth" then log.trace("deleting Force OAuth") self.oauth.force_oauth = nil @@ -639,11 +650,13 @@ end function SonosDriver.new_driver_template() local oauth_token_bus = cosock.bus() + local oauth_info_bus = cosock.bus() local template = { sonos = SonosState.instance(), discovery = SonosDisco.discover, oauth_token_bus = oauth_token_bus, + oauth_info_bus = oauth_info_bus, oauth = {}, waiting_for_oauth_token = false, startup_state_received = false, From 5e49828b4ead33110ac2f8a57de11f32b154bd8a Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:46:01 -0500 Subject: [PATCH 113/449] sonos: Only return oauth token if oauth is connected --- .../SmartThings/sonos/src/sonos_driver.lua | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 1451908bd1..9dfe5f1d46 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -189,9 +189,7 @@ end ---@param update_key "endpointAppInfo"|"sonosOAuthToken" ---@param update_value string function SonosDriver:notify_augmented_data_changed(update_kind, update_key, update_value) - local already_connected = self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" + local already_connected = self:oauth_is_connected() log.info(string.format("Already connected? %s", already_connected)) if update_kind == "snapshot" then self:update_after_startup_state_received() @@ -251,12 +249,7 @@ end function SonosDriver:check_auth(info_or_device) local maybe_token, _ = self:get_oauth_token() - local token_valid = (api_version >= 14 and security ~= nil) - and self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and maybe_token ~= nil - if token_valid then + if maybe_token then return true, SonosApi.api_keys.oauth_key elseif self.oauth.force_oauth then return false @@ -369,23 +362,14 @@ function SonosDriver:request_oauth_token() end ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ----@return "token expired"|"no token"|"not supported"|nil reason the reason a token was not provided, nil if there is a valid token available +---@return "token expired"|"no token"|"not supported"|"not connected"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() if api_version < 14 or security == nil then return nil, "not supported" end - self.hub_augmented_driver_data = self.hub_augmented_driver_data or {} - local decode_success, maybe_token = - pcall(json.decode, self.hub_augmented_driver_data.sonosOAuthToken) - if - decode_success - and type(maybe_token) == "table" - and type(maybe_token.accessToken) == "string" - and type(maybe_token.expiresAt) == "number" - then - self.oauth.token = maybe_token - elseif self.hub_augmented_driver_data.sonosOAuthToken ~= nil then - log.warn(string.format("Unable to JSON decode token from hub augmented data: %s", maybe_token)) + + if not self:oauth_is_connected() then + return nil, "not connected" end if self.oauth.token then @@ -402,6 +386,13 @@ function SonosDriver:get_oauth_token() return nil, "no token" end +function SonosDriver:oauth_is_connected() + return (api_version >= 14 and security ~= nil) + and self.oauth + and self.oauth.endpoint_app_info + and self.oauth.endpoint_app_info.state == "connected" +end + ---Create a cosock task that handles events from the persistent SSDP task. ---@param driver SonosDriver ---@param discovery_event_subscription cosock.Bus.Subscription From b2e489f3210e263a032cd59fa914a61b8817ec55 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:48:21 -0500 Subject: [PATCH 114/449] sonos: Add persistent token refresher task --- .../SmartThings/sonos/src/sonos_driver.lua | 4 + .../SmartThings/sonos/src/token_refresher.lua | 152 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 drivers/SmartThings/sonos/src/token_refresher.lua diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 9dfe5f1d46..3404dca569 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -222,6 +222,10 @@ end function SonosDriver:handle_startup_state_received() self:start_ssdp_event_task() self:notify_augmented_data_changed "snapshot" + if api_version >= 14 and security ~= nil then + local token_refresher = require "token_refresher" + token_refresher.spawn_token_refresher(self) + end self.startup_state_received = true for _, device in pairs(self.devices_waiting_for_startup_state) do SonosDriverLifecycleHandlers.initialize_device(self, device) diff --git a/drivers/SmartThings/sonos/src/token_refresher.lua b/drivers/SmartThings/sonos/src/token_refresher.lua new file mode 100644 index 0000000000..0734347795 --- /dev/null +++ b/drivers/SmartThings/sonos/src/token_refresher.lua @@ -0,0 +1,152 @@ +local cosock = require "cosock" +local utils = require "utils" +local security = require "st.security" +local log = require "log" + +local module = {} + +local ACTIONS = { + -- This action waits for an event via the oauth endpoint app info bus in order to determine + -- when the driver goes from a disconnected to connected state. + WAIT_FOR_CONNECTED = 1, + -- This action will wait for the current valid token to expire. It will also handle a new token + -- event to redetermine the current action. New tokens that come in during this action will likely + -- be from debug testing. + WAIT_FOR_EXPIRE = 2, + -- This action requests a new token and waits for it to come in. + REQUEST_TOKEN = 3, +} + +local ACTION_STRINGIFY = { + [ACTIONS.WAIT_FOR_CONNECTED] = "wait for connected", + [ACTIONS.WAIT_FOR_EXPIRE] = "wait for expire", + [ACTIONS.REQUEST_TOKEN] = "request token", +} + +local Refresher = {} +Refresher.__index = Refresher + +--- Determine which action the refresher should take. +--- This just depends on: +--- - Is Oauth connected? +--- - Do we have a valid token? +function Refresher:determine_action() + if not self.driver:oauth_is_connected() then + -- Oauth is disconnected so no point in trying to request a token until we are connected. + return ACTIONS.WAIT_FOR_CONNECTED + end + local token, _ = self.driver:get_oauth_token() + if token then + local now = os.time() + local expiration = math.floor(token.expiresAt / 1000) + if (expiration - now) > 60 then + -- Token is valid and not expiring in the next 60 seconds. + return ACTIONS.WAIT_FOR_EXPIRE + end + end + -- We don't have a valid token or it is about to expire soon. + return ACTIONS.REQUEST_TOKEN +end + +--- Waits for a token event with a timeout. +--- @param timeout number How long the function will wait for a new token +function Refresher:try_wait_for_token_event(timeout) + local token_bus, err = self.driver:oauth_token_event_subscribe() + if err == "closed" then + self.token_bus_closed = true + end + if token_bus then + token_bus:settimeout(timeout) + token_bus:receive() + end +end + +--- Waits for the current token to expire or a new token event. +--- +--- The likely outcome of this function is to wait the entire expiration timeout. It will +--- also listen for token events just in case a new token with a new expiration is sent to the driver. +--- A new token would most likely come from developer testing, but since the new token requests are +--- not synchronous one could come from an earlier request. +function Refresher:wait_for_expire_or_token_event() + local maybe_token, err = self.driver:get_oauth_token() + if not maybe_token then + -- Something got funky in the state machine, return and re-determine our next action + log.warn(string.format("Tried to wait for expiration of non-existent token: %s", err)) + return + end + -- The token will be refreshed if requested within 1 minute of expiration + local expiration = math.floor(maybe_token.expiresAt / 1000) - 60 + local now = os.time() + local timeout = math.max(expiration - now, 0) + + log.debug(string.format("Token will refresh in %d seconds", timeout)) + -- Wait while trying to receive a token event in case it gets updated for some reason. + self:try_wait_for_token_event(timeout) +end + +--- Waits for an oauth endpoint app info event indefinitely. +--- +--- A new info event indicates that `Refresher:determine_action` should be called to check if oauth +--- is now connected. +function Refresher:wait_for_info_event() + local info_sub, err = self.driver:oauth_info_event_subscribe() + if err == "closed" then + self.info_bus_closed = true + end + if info_sub then + info_sub:receive() + end +end + +--- Requests a token then waits for a new token event. +function Refresher:request_token() + local result, err = security.get_sonos_oauth() + if not result then + log.warn(string.format("Failed to request oauth token: %s", err)) + end + -- Try to receive token even if the request failed. + self:try_wait_for_token_event(10) + local maybe_token, _ = self.driver:get_oauth_token() + if maybe_token then + -- token is valid, reset backoff + self.token_backoff = utils.backoff_builder(30 * 60, 5, 0.1) + else + -- We either didn't receive a token or it is not valid. + -- Backoff and maybe we will receive it in that time, or we retry. + cosock.socket.sleep(self.token_backoff()) + end +end + +function module.spawn_token_refresher(driver) + local refresher = setmetatable({ driver = driver, + token_backoff = utils.backoff_builder(30 * 60, 5, 0.1), + }, + Refresher) + cosock.spawn(function () + while true do + -- We can always determine what we should be doing based off the information we have, + -- any action can proceed action depending on what needs to be done. + local action = refresher:determine_action() + log.info(string.format("Token refresher action: %s", ACTION_STRINGIFY[action])) + if action == ACTIONS.WAIT_FOR_CONNECTED then + refresher:wait_for_info_event() + elseif action == ACTIONS.WAIT_FOR_EXPIRE then + refresher:wait_for_expire_or_token_event() + elseif action == ACTIONS.REQUEST_TOKEN then + refresher:request_token() + else + log.error(string.format("Token refresher task exiting due to bad token refresher action: %s", action)) + return + end + if refresher.token_bus_closed or refresher.info_bus_closed then + log.error(string.format("Token refresher task exiting. Token bus closed: %s Info bus close: %s", + refresher.token_bus_closed, refresher.info_bus_closed)) + return + end + end + end, "token refresher task") +end + +return module + + From ecd1d603afac7fd047d64baf6fc1949b0ff120e4 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:50:20 -0500 Subject: [PATCH 115/449] sonos: remove token requests outside of the token refresher task --- .../sonos/src/api/sonos_connection.lua | 21 +++------- .../sonos/src/lifecycle_handlers.lua | 19 ---------- .../SmartThings/sonos/src/sonos_driver.lua | 38 ------------------- 3 files changed, 5 insertions(+), 73 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index d854401313..28ce57e441 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -246,11 +246,7 @@ end ---@param sonos_conn SonosConnection local function _oauth_reconnect_task(sonos_conn) log.debug("Spawning reconnect task for ", sonos_conn.device.label) - local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - local unauthorized = (check_auth == false) - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end + -- Subscribe first so we get all updates on the bus local token_receive_handle, err = sonos_conn.driver:oauth_token_event_subscribe() if not token_receive_handle then log.warn(string.format("error creating oauth token receive handle for respawn task: %s", err)) @@ -258,7 +254,10 @@ local function _oauth_reconnect_task(sonos_conn) cosock.spawn(function() local backoff = backoff_builder(60, 1, 0.1) while not sonos_conn:is_running() do - if sonos_conn.driver:is_waiting_for_oauth_token() and token_receive_handle then + local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + local unauthorized = (check_auth == false) + + if unauthorized then local token, channel_error = token_receive_handle:receive() if not token then log.warn(string.format("Error requesting token: %s", channel_error)) @@ -275,12 +274,6 @@ local function _oauth_reconnect_task(sonos_conn) end end - check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - unauthorized = (check_auth == false) - - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end cosock.socket.sleep(backoff()) end sonos_conn._reconnecting = false @@ -346,10 +339,6 @@ function SonosConnection.new(driver, device) device.log.warn( string.format("WebSocket connection no longer authorized, disconnecting") ) - local _, security_err = driver:request_oauth_token() - if security_err then - log.warn(string.format("Error during request for oauth token: %s", security_err)) - end -- closing the socket directly without calling `:stop()` triggers the reconnect loop, -- which is where we wait for the token to come in. local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id) diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index c62b90abcd..2b38505875 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -82,16 +82,9 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local token, token_recv_err -- max 30 mins local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn(string.format("Error sending token request: %s", request_token_err)) - end - end local backoff_timer = nil while not token do - local send_request = false -- we use the backoff to create a timer and utilize a select loop here, instead of -- utilizing a sleep, so that we can create a long delay on our polling of the cloud -- without putting ourselves in a situation where we're sleeping for an extended period @@ -117,7 +110,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) -- -- This is just in case both receivers are ready, so that we can prioritize -- handling the token instead of putting another request in flight. - send_request = true backoff_timer:handled() backoff_timer = nil end @@ -137,17 +129,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) ) ) end - - if send_request then - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn( - string.format("Error sending token request: %s", request_token_err) - ) - end - end - end end else device.log.error( diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 3404dca569..9ef3f28dc9 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -114,11 +114,6 @@ function SonosDriver:handle_augmented_data_change(update_key, decoded) end end ----@return boolean -function SonosDriver:is_waiting_for_oauth_token() - return (api_version >= 14 and security ~= nil) and self.waiting_for_oauth_token -end - ---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed ---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success function SonosDriver:oauth_token_event_subscribe() @@ -206,17 +201,6 @@ function SonosDriver:notify_augmented_data_changed(update_kind, update_key, upda ) ) end - - if - self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and not already_connected - then - local _, err = self:request_oauth_token() - if err then - log.error(string.format("Request OAuth token error: %s", err)) - end - end end function SonosDriver:handle_startup_state_received() @@ -344,27 +328,6 @@ function SonosDriver:check_auth(info_or_device) ) end ----@return any? ret nil on permissions violation ----@return string? error nil on success -function SonosDriver:request_oauth_token() - if api_version < 14 or security == nil then - return nil, "not supported" - end - local maybe_token, maybe_err = self:get_oauth_token() - if maybe_err then - log.warn(string.format("get oauth token error: %s", maybe_err)) - end - if type(maybe_token) == "table" and type(maybe_token.accessToken) == "string" then - self.oauth_token_bus:send(maybe_token) - end - local result, err = security.get_sonos_oauth() - if not result then - return nil, string.format("Error requesting OAuth token via Security API: %s", err) - end - self.waiting_for_oauth_token = true - return result, err -end - ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ---@return "token expired"|"no token"|"not supported"|"not connected"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() @@ -653,7 +616,6 @@ function SonosDriver.new_driver_template() oauth_token_bus = oauth_token_bus, oauth_info_bus = oauth_info_bus, oauth = {}, - waiting_for_oauth_token = false, startup_state_received = false, devices_waiting_for_startup_state = {}, bonded_devices = utils.new_mac_address_keyed_table(), From 38271456341afb48ab7aba880e494130a5c2f5a7 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:51:10 -0500 Subject: [PATCH 116/449] sonos: Wait for valid token before sending commands --- .../sonos/src/api/cmd_handlers.lua | 8 +++--- .../SmartThings/sonos/src/sonos_driver.lua | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua index c2de236044..b5ccfe10b9 100644 --- a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua @@ -24,9 +24,9 @@ local function _do_send_to_group(driver, device, payload) local household_id, group_id = driver.sonos:get_group_for_device(device) payload[1].householdId = household_id payload[1].groupId = group_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then @@ -40,9 +40,9 @@ local function _do_send_to_self(driver, device, payload) local household_id, player_id = driver.sonos:get_player_for_device(device) payload[1].householdId = household_id payload[1].playerId = player_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 9ef3f28dc9..70cd626566 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -353,6 +353,34 @@ function SonosDriver:get_oauth_token() return nil, "no token" end +function SonosDriver:wait_for_oauth_token(timeout) + if api_version < 14 or security == nil then + return nil, "not supported" + end + + if not self:oauth_is_connected() then + return nil, "not connected" + end + + -- See if a valid token is already available + local maybe_token, _ = self:get_oauth_token() + if maybe_token then + -- return the valid token + return maybe_token + end + -- Subscribe to the token event bus. A new token has been/will be requested + -- by the token refresher task. + local token_bus, err = self:oauth_token_event_subscribe() + if token_bus then + token_bus:settimeout(timeout) + -- Wait for the new token to come in + token_bus:receive() + -- Call `SonosDriver:get_oauth_token` again to ensure the token is valid. + return self:get_oauth_token() + end + return nil, err +end + function SonosDriver:oauth_is_connected() return (api_version >= 14 and security ~= nil) and self.oauth From 664efa17805093ff7d0f6d790381540e3d806e08 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 2 Sep 2025 09:54:11 -0500 Subject: [PATCH 117/449] sonos: Update oauth_is_connected to oauth_app_connected --- drivers/SmartThings/sonos/src/sonos_driver.lua | 8 ++++---- drivers/SmartThings/sonos/src/token_refresher.lua | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 70cd626566..d496070063 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -184,7 +184,7 @@ end ---@param update_key "endpointAppInfo"|"sonosOAuthToken" ---@param update_value string function SonosDriver:notify_augmented_data_changed(update_kind, update_key, update_value) - local already_connected = self:oauth_is_connected() + local already_connected = self:oauth_app_connected() log.info(string.format("Already connected? %s", already_connected)) if update_kind == "snapshot" then self:update_after_startup_state_received() @@ -335,7 +335,7 @@ function SonosDriver:get_oauth_token() return nil, "not supported" end - if not self:oauth_is_connected() then + if not self:oauth_app_connected() then return nil, "not connected" end @@ -358,7 +358,7 @@ function SonosDriver:wait_for_oauth_token(timeout) return nil, "not supported" end - if not self:oauth_is_connected() then + if not self:oauth_app_connected() then return nil, "not connected" end @@ -381,7 +381,7 @@ function SonosDriver:wait_for_oauth_token(timeout) return nil, err end -function SonosDriver:oauth_is_connected() +function SonosDriver:oauth_app_connected() return (api_version >= 14 and security ~= nil) and self.oauth and self.oauth.endpoint_app_info diff --git a/drivers/SmartThings/sonos/src/token_refresher.lua b/drivers/SmartThings/sonos/src/token_refresher.lua index 0734347795..2ba3ad660e 100644 --- a/drivers/SmartThings/sonos/src/token_refresher.lua +++ b/drivers/SmartThings/sonos/src/token_refresher.lua @@ -31,7 +31,7 @@ Refresher.__index = Refresher --- - Is Oauth connected? --- - Do we have a valid token? function Refresher:determine_action() - if not self.driver:oauth_is_connected() then + if not self.driver:oauth_app_connected() then -- Oauth is disconnected so no point in trying to request a token until we are connected. return ACTIONS.WAIT_FOR_CONNECTED end From c707fe72340b79dd3cd876c706aef318522a9da7 Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Thu, 11 Sep 2025 12:55:30 -0500 Subject: [PATCH 118/449] Update xCREAS Air Purifier fingerprint to static profile (#2390) --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index e25443d49c..b45204c762 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -60,7 +60,7 @@ matterManufacturer: deviceLabel: xCREAS Smart Air Purifier Cyclone400 vendorId: 0x156A productId: 0x0001 - deviceProfileName: air-purifier-modular + deviceProfileName: air-purifier-hepa-wind-aqs-pm25-meas-pm25-level matterGeneric: - id: "matter/hvac/heatcool" deviceLabel: Matter Thermostat From d1c82fb1b063980b80ccff4b0b991f4e91b01671 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 11 Sep 2025 14:08:24 -0500 Subject: [PATCH 119/449] sonos: Fix oauth account linking notifications --- .../sonos/src/api/sonos_connection.lua | 2 ++ .../sonos/src/lifecycle_handlers.lua | 1 + .../SmartThings/sonos/src/sonos_driver.lua | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 28ce57e441..35b1cf8885 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -258,6 +258,7 @@ local function _oauth_reconnect_task(sonos_conn) local unauthorized = (check_auth == false) if unauthorized then + sonos_conn.driver:alert_unauthorized() local token, channel_error = token_receive_handle:receive() if not token then log.warn(string.format("Error requesting token: %s", channel_error)) @@ -347,6 +348,7 @@ function SonosConnection.new(driver, device) else Router.close_socket_for_player(unique_key) end + self.driver:alert_unauthorized() end end elseif header.type == "groups" then diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 2b38505875..0db0efc2c9 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -82,6 +82,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local token, token_recv_err -- max 30 mins local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) + driver:alert_unauthorized() local backoff_timer = nil while not token do diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index d496070063..82d88752f2 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -40,6 +40,7 @@ end ---@field private oauth { token: {accessToken: string, expiresAt: number}, endpoint_app_info: { state: "connected"|"disconnected" }, force_oauth: boolean? } cached OAuth info ---@field private startup_state_received boolean ---@field private devices_waiting_for_startup_state SonosDevice[] +---@field private have_alerted_unauthorized boolean Used to track if we have requested an oauth token, this will trigger the notification used for account linking ---@field package bonded_devices table map of Device device_network_id to a boolean indicating if the device is currently known as a bonded device. --- ---@field public ssdp_task SonosPersistentSsdpTask? @@ -388,6 +389,25 @@ function SonosDriver:oauth_app_connected() and self.oauth.endpoint_app_info.state == "connected" end +--- Used to trigger the notification that the user must link their sonos account. +--- Will request a token a single time which will trigger preinstall isa flow. +function SonosDriver:alert_unauthorized() + if api_version < 14 or security == nil then + return + end + if self.have_alerted_unauthorized then + return + end + -- Do the request regardless if we think oauth is connected, because + -- there is a possibility that we have stale data. + local result, err = security.get_sonos_oauth() + if not result then + log.warn(string.format("Failed to alert unauthorized: %s", err)) + return + end + self.have_alerted_unauthorized = true +end + ---Create a cosock task that handles events from the persistent SSDP task. ---@param driver SonosDriver ---@param discovery_event_subscription cosock.Bus.Subscription @@ -644,6 +664,7 @@ function SonosDriver.new_driver_template() oauth_token_bus = oauth_token_bus, oauth_info_bus = oauth_info_bus, oauth = {}, + have_alerted_unauthorized = false, startup_state_received = false, devices_waiting_for_startup_state = {}, bonded_devices = utils.new_mac_address_keyed_table(), From e2403f2ad35b89b4943824597939bdb21fac5f0f Mon Sep 17 00:00:00 2001 From: GAFfrient <96058156+GAFfrient@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:52:30 +0200 Subject: [PATCH 120/449] WWSTCERT-7636 Frient humidity sensor (hmszb 120/hmszb 110) add support (#2356) * Add support for HMSZB-120/HMSZB-110 * According to @greens feedback. * Whitespace * Updated test message. * Fix info_changed part. * Whitespace. * Fix test. * Fix test 2 * Whitespace fix. --------- Co-authored-by: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> --- .../zigbee-humidity-sensor/fingerprints.yml | 9 +- .../frient-humidity-temperature-battery.yml | 39 +++++ .../src/configurations.lua | 21 ++- .../src/frient-sensor/init.lua | 34 ++++- .../src/test/test_frient_sensor.lua | 136 ++++++++++++++++-- 5 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index c6dd3efcca..9ab5af08d6 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -59,10 +59,15 @@ zigbeeManufacturer: model: HT-EF-3.0 deviceProfileName: humidity-temp-battery - id: frient/HMSZB-110 - deviceLabel: frient Multipurpose Sensor + deviceLabel: frient Humidity Sensor manufacturer: frient A/S model: HMSZB-110 - deviceProfileName: humidity-temp-battery + deviceProfileName: frient-humidity-temperature-battery + - id: frient/HMSZB-120 + deviceLabel: frient Humidity Sensor + manufacturer: frient A/S + model: HMSZB-120 + deviceProfileName: frient-humidity-temperature-battery - id: eWeLink/TH01 deviceLabel: eWeLink Multipurpose Sensor manufacturer: eWeLink diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml new file mode 100644 index 0000000000..cd186812bf --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml @@ -0,0 +1,39 @@ +name: frient-humidity-temperature-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: TempHumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index 5c8f6834e9..3a4e495e3e 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -22,24 +22,33 @@ local PowerConfiguration = clusters.PowerConfiguration local devices = { FRIENT_HUMIDITY_TEMP_SENSOR = { FINGERPRINTS = { - { mfr = "frient A/S", model = "HMSZB-110" } + { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" } }, CONFIGURATION = { { cluster = RelativeHumidity.ID, attribute = RelativeHumidity.attributes.MeasuredValue.ID, minimum_interval = 60, - maximum_interval = 600, + maximum_interval = 3600, data_type = RelativeHumidity.attributes.MeasuredValue.base_type, - reportable_change = 100 + reportable_change = 300 }, { cluster = TemperatureMeasurement.ID, attribute = TemperatureMeasurement.attributes.MeasuredValue.ID, - minimum_interval = 60, - maximum_interval = 600, + minimum_interval = 30, + maximum_interval = 3600, data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type, - reportable_change = 10 + reportable_change = 100 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30 , + maximum_interval = 21600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 } } }, diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index c63b319cea..a56d070daa 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -14,9 +14,13 @@ local battery_defaults = require "st.zigbee.defaults.battery_defaults" local configurationMap = require "configurations" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" } } local function can_handle_frient_sensor(opts, driver, device) @@ -39,10 +43,36 @@ local function device_init(driver, device) end end +local function do_configure(driver, device, event, args) + device:configure() + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + for name, value in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local sensitivity = math.floor((device.preferences[name]) * 100 + 0.5) + if (name == "temperatureSensitivity") then + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, sensitivity)) + end + if (name == "humiditySensitivity") then + device:send(HumidityMeasurement.attributes.MeasuredValue:configure_reporting(device, 60, 3600, sensitivity)) + end + end + end + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + local frient_sensor = { NAME = "Frient Humidity Sensor", lifecycle_handlers = { - init = device_init + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed }, can_handle = can_handle_frient_sensor } 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 e61ae9e22d..c2723d0edd 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 @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -16,21 +16,22 @@ 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 TemperatureMeasurement = clusters.TemperatureMeasurement -local RelativeHumidity = clusters.RelativeHumidity +local HumidityMeasurement = clusters.RelativeHumidity local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("humidity-temp-battery.yml"), + profile = t_utils.get_profile_definition("frient-humidity-temperature-battery.yml"), zigbee_endpoints = { - [1] = { - id = 1, + [26] = { + id = 26, manufacturer = "frient A/S", - model = "HMSZB-110", + model = "HMSZB-120", server_clusters = {0x0001, 0x0402, 0x0405} - } + }, } } ) @@ -63,7 +64,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - RelativeHumidity.attributes.MeasuredValue:read(mock_device) + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) } }, { @@ -80,6 +81,38 @@ test.register_message_test( } ) +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + test.register_coroutine_test( "Configure should configure all necessary attributes", function() @@ -89,7 +122,7 @@ test.register_coroutine_test( mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, - RelativeHumidity.ID + HumidityMeasurement.ID ) }) test.socket.zigbee:__expect_send({ @@ -108,7 +141,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 100) + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -116,13 +149,86 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 10) + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) }) - test.socket.zigbee:__expect_send({ mock_device.id, RelativeHumidity.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) -test.run_registered_tests() +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + end +) + +test.run_registered_tests() \ No newline at end of file From dba5fc628236685e469aaa1754799d73eb6a7a08 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 12 Sep 2025 13:40:00 -0700 Subject: [PATCH 121/449] WWSTCERT-7632 LUX TQ1 Smart Thermostat (#2361) * WWSTCERT-7632 LUX TQ1 Smart Thermostat * change profile * Revert "change profile" This reverts commit 4a2e42b1fc7a87b3df7bd104c08a2841c8f7e386. --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index b45204c762..7015cc6431 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -38,6 +38,12 @@ matterManufacturer: vendorId: 0x1527 productId: 0x0002 deviceProfileName: thermostat-heating-only-batteryLevel + #Lux + - id: "4614/1" + deviceLabel: LUX TQ1 Smart Thermostat + vendorId: 0x1206 + productId: 0x0001 + deviceProfileName: thermostat-nostate-nobattery #Siterwell - id: "4736/769" deviceLabel: Siterwell Radiator Thermostat From 68446131e1b337a2311049bc5fa10ef1d7031d54 Mon Sep 17 00:00:00 2001 From: KyuminAhn <46492097+KyuminAhn@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:13:39 +0900 Subject: [PATCH 122/449] WWSTCERT-7844 Feature/matter britzyhub (#2189) --- drivers/SinuxSoft/britzyhub/README.md | 9 + drivers/SinuxSoft/britzyhub/config.yml | 6 + drivers/SinuxSoft/britzyhub/fingerprints.yml | 16 ++ .../SinuxSoft/britzyhub/profiles/elevator.yml | 10 ++ .../britzyhub/profiles/gas-valve.yml | 10 ++ drivers/SinuxSoft/britzyhub/profiles/vent.yml | 19 ++ .../SinuxSoft/britzyhub/src/elevator/init.lua | 91 ++++++++++ .../britzyhub/src/gas-valve/init.lua | 91 ++++++++++ drivers/SinuxSoft/britzyhub/src/init.lua | 16 ++ drivers/SinuxSoft/britzyhub/src/vent/init.lua | 164 ++++++++++++++++++ 10 files changed, 432 insertions(+) create mode 100644 drivers/SinuxSoft/britzyhub/README.md create mode 100644 drivers/SinuxSoft/britzyhub/config.yml create mode 100644 drivers/SinuxSoft/britzyhub/fingerprints.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/elevator.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/vent.yml create mode 100644 drivers/SinuxSoft/britzyhub/src/elevator/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/vent/init.lua diff --git a/drivers/SinuxSoft/britzyhub/README.md b/drivers/SinuxSoft/britzyhub/README.md new file mode 100644 index 0000000000..510501f7cb --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/README.md @@ -0,0 +1,9 @@ +# BritzyHub Matter Edge Driver + +Edge driver for registering BritzyHub Matter dedicated Matter devices. + +--- + +## Reference + +- Use CLI.md for detailed SmartThings CLI command summaries diff --git a/drivers/SinuxSoft/britzyhub/config.yml b/drivers/SinuxSoft/britzyhub/config.yml new file mode 100644 index 0000000000..21551f1849 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/config.yml @@ -0,0 +1,6 @@ +name: 'BritzyHub Matter' +packageKey: 'britzyhub-matter' +permissions: + matter: {} +description: "SmartThings Edge driver for BritzyHub Matter devices." +vendorSupportInformation: "https://sinux.kr" \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/fingerprints.yml b/drivers/SinuxSoft/britzyhub/fingerprints.yml new file mode 100644 index 0000000000..904b544db8 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/fingerprints.yml @@ -0,0 +1,16 @@ +matterGeneric: + - id: "elevator" + deviceLabel: Elevator + deviceTypes: + - id: 0xFF02 + deviceProfileName: elevator + - id: "gas-valve" + deviceLabel: Gas Valve + deviceTypes: + - id: 0xFF01 + deviceProfileName: gas-valve + - id: "vent" + deviceLabel: Vent + deviceTypes: + - id: 0xFF03 + deviceProfileName: vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/elevator.yml b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml new file mode 100644 index 0000000000..a120b501dc --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml @@ -0,0 +1,10 @@ +name: elevator +components: + - id: main + capabilities: + - id: elevatorCall + version: 1 + - id: refresh + version: 1 + categories: + - name: Elevator \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml new file mode 100644 index 0000000000..30f18c453b --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml @@ -0,0 +1,10 @@ +name: gas-valve +components: + - id: main + capabilities: + - id: safetyValve + version: 1 + - id: refresh + version: 1 + categories: + - name: GasValve \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/vent.yml b/drivers/SinuxSoft/britzyhub/profiles/vent.yml new file mode 100644 index 0000000000..2c956b307f --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/vent.yml @@ -0,0 +1,19 @@ +name: vent +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: fanMode + version: 1 + config: + values: + - key: "fanMode.value" + enabledValues: + - low + - medium + - high + - id: refresh + version: 1 + categories: + - name: Vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/elevator/init.lua b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua new file mode 100644 index 0000000000..51aa1d8e0e --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local elevator_cap = capabilities.elevatorCall +local onoff_cluster = clusters.OnOff + +local ELEVATOR_DEVICE_TYPE_ID = 0xFF02 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.called()) + else + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.standby()) + end +end + +local function handle_elevator_call(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.On(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, clusters.OnOff.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_elevator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == ELEVATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local elevator_handler = { + NAME = "Elevator Handler", + can_handle = is_matter_elevator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [elevator_cap.ID] = { + [elevator_cap.commands.call.NAME] = handle_elevator_call, + } + }, + supported_capabilities = { + elevator_cap, + }, + subscribed_attributes = { + [elevator_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return elevator_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua new file mode 100644 index 0000000000..52dd3cafb6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local valve_cap = capabilities.safetyValve +local onoff_cluster = clusters.OnOff + +local GAS_VALVE_DEVICE_TYPE_ID = 0xFF01 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.closed()) + end +end + +local function handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.Off(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, onoff_cluster.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_gas_valve(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == GAS_VALVE_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local gas_valve_handler = { + NAME = "Gas Valve Handler", + can_handle = is_matter_gas_valve, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [valve_cap.ID] = { + [valve_cap.commands.close.NAME] = handle_valve_close, + } + }, + supported_capabilities = { + valve_cap, + }, + subscribed_attributes = { + [valve_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return gas_valve_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/init.lua b/drivers/SinuxSoft/britzyhub/src/init.lua new file mode 100644 index 0000000000..60f3cb90ce --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/init.lua @@ -0,0 +1,16 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local MatterDriver = require "st.matter.driver" +local log = require "log" + +local matter_driver = MatterDriver("britzyhub-matter", { + sub_drivers = { + require ("elevator"), + require ("gas-valve"), + require ("vent"), + } +}) + +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/vent/init.lua b/drivers/SinuxSoft/britzyhub/src/vent/init.lua new file mode 100644 index 0000000000..93e11f40c6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/vent/init.lua @@ -0,0 +1,164 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local VENTILATOR_DEVICE_TYPE_ID = 0xFF03 + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" + +local subscribed_attributes = { + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, +} + +local function map_enum_value(map, value, default) + return map[value] or default +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then + return v + end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return find_default_endpoint(device, cluster_id) +end + +local endpoint_to_component = function(device, endpoint_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +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.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +local AC_FAN_MODE_MAP = { + [clusters.FanControl.attributes.FanMode.LOW] = capabilities.fanMode.fanMode.low(), + [clusters.FanControl.attributes.FanMode.MEDIUM] = capabilities.fanMode.fanMode.medium(), + [clusters.FanControl.attributes.FanMode.HIGH] = capabilities.fanMode.fanMode.high(), +} + +local function fan_mode_handler(driver, device, ib, response) + local cap = capabilities.fanMode + if device:supports_capability_by_id(cap.ID) then + local event = map_enum_value(AC_FAN_MODE_MAP, ib.data.value, cap.fanMode.low()) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end +end + +local function handle_switch_on(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.On(device, ep)) +end + +local function handle_switch_off(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.Off(device, ep)) +end + +local function set_fan_mode(driver, device, cmd) + local args = cmd.args.fanMode + local mode_map = { + low = clusters.FanControl.attributes.FanMode.LOW, + medium = clusters.FanControl.attributes.FanMode.MEDIUM, + high = clusters.FanControl.attributes.FanMode.HIGH, + } + local ep = component_to_endpoint(device, cmd.component, clusters.FanControl.ID) + local mode = mode_map[args] or clusters.FanControl.attributes.FanMode.OFF + device:send(clusters.FanControl.attributes.FanMode:write(device, ep, mode)) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local function info_changed(driver, device, event, args) + 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:subscribe() +end + +local function is_matter_ventilator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == VENTILATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local ventilator_handler = { + NAME = "Ventilator Handler", + can_handle = is_matter_ventilator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, + }, + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_switch_on, + [capabilities.switch.commands.off.NAME] = handle_switch_off, + }, + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.fanMode, + }, + subscribed_attributes = subscribed_attributes +} + +return ventilator_handler \ No newline at end of file From 09b90d930a94babeb4c87b82ff7d496b03e7ef44 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 12 Sep 2025 13:40:00 -0700 Subject: [PATCH 123/449] WWSTCERT-7632 LUX TQ1 Smart Thermostat (#2361) * WWSTCERT-7632 LUX TQ1 Smart Thermostat * change profile * Revert "change profile" This reverts commit 4a2e42b1fc7a87b3df7bd104c08a2841c8f7e386. --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index e25443d49c..1091984b73 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -38,6 +38,12 @@ matterManufacturer: vendorId: 0x1527 productId: 0x0002 deviceProfileName: thermostat-heating-only-batteryLevel + #Lux + - id: "4614/1" + deviceLabel: LUX TQ1 Smart Thermostat + vendorId: 0x1206 + productId: 0x0001 + deviceProfileName: thermostat-nostate-nobattery #Siterwell - id: "4736/769" deviceLabel: Siterwell Radiator Thermostat From 582d92fc348b85d0911ebd4ca1c908cf42c4b1e4 Mon Sep 17 00:00:00 2001 From: KyuminAhn <46492097+KyuminAhn@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:13:39 +0900 Subject: [PATCH 124/449] WWSTCERT-7844 Feature/matter britzyhub (#2189) --- drivers/SinuxSoft/britzyhub/README.md | 9 + drivers/SinuxSoft/britzyhub/config.yml | 6 + drivers/SinuxSoft/britzyhub/fingerprints.yml | 16 ++ .../SinuxSoft/britzyhub/profiles/elevator.yml | 10 ++ .../britzyhub/profiles/gas-valve.yml | 10 ++ drivers/SinuxSoft/britzyhub/profiles/vent.yml | 19 ++ .../SinuxSoft/britzyhub/src/elevator/init.lua | 91 ++++++++++ .../britzyhub/src/gas-valve/init.lua | 91 ++++++++++ drivers/SinuxSoft/britzyhub/src/init.lua | 16 ++ drivers/SinuxSoft/britzyhub/src/vent/init.lua | 164 ++++++++++++++++++ 10 files changed, 432 insertions(+) create mode 100644 drivers/SinuxSoft/britzyhub/README.md create mode 100644 drivers/SinuxSoft/britzyhub/config.yml create mode 100644 drivers/SinuxSoft/britzyhub/fingerprints.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/elevator.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml create mode 100644 drivers/SinuxSoft/britzyhub/profiles/vent.yml create mode 100644 drivers/SinuxSoft/britzyhub/src/elevator/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/init.lua create mode 100644 drivers/SinuxSoft/britzyhub/src/vent/init.lua diff --git a/drivers/SinuxSoft/britzyhub/README.md b/drivers/SinuxSoft/britzyhub/README.md new file mode 100644 index 0000000000..510501f7cb --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/README.md @@ -0,0 +1,9 @@ +# BritzyHub Matter Edge Driver + +Edge driver for registering BritzyHub Matter dedicated Matter devices. + +--- + +## Reference + +- Use CLI.md for detailed SmartThings CLI command summaries diff --git a/drivers/SinuxSoft/britzyhub/config.yml b/drivers/SinuxSoft/britzyhub/config.yml new file mode 100644 index 0000000000..21551f1849 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/config.yml @@ -0,0 +1,6 @@ +name: 'BritzyHub Matter' +packageKey: 'britzyhub-matter' +permissions: + matter: {} +description: "SmartThings Edge driver for BritzyHub Matter devices." +vendorSupportInformation: "https://sinux.kr" \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/fingerprints.yml b/drivers/SinuxSoft/britzyhub/fingerprints.yml new file mode 100644 index 0000000000..904b544db8 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/fingerprints.yml @@ -0,0 +1,16 @@ +matterGeneric: + - id: "elevator" + deviceLabel: Elevator + deviceTypes: + - id: 0xFF02 + deviceProfileName: elevator + - id: "gas-valve" + deviceLabel: Gas Valve + deviceTypes: + - id: 0xFF01 + deviceProfileName: gas-valve + - id: "vent" + deviceLabel: Vent + deviceTypes: + - id: 0xFF03 + deviceProfileName: vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/elevator.yml b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml new file mode 100644 index 0000000000..a120b501dc --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/elevator.yml @@ -0,0 +1,10 @@ +name: elevator +components: + - id: main + capabilities: + - id: elevatorCall + version: 1 + - id: refresh + version: 1 + categories: + - name: Elevator \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml new file mode 100644 index 0000000000..30f18c453b --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/gas-valve.yml @@ -0,0 +1,10 @@ +name: gas-valve +components: + - id: main + capabilities: + - id: safetyValve + version: 1 + - id: refresh + version: 1 + categories: + - name: GasValve \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/profiles/vent.yml b/drivers/SinuxSoft/britzyhub/profiles/vent.yml new file mode 100644 index 0000000000..2c956b307f --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/profiles/vent.yml @@ -0,0 +1,19 @@ +name: vent +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: fanMode + version: 1 + config: + values: + - key: "fanMode.value" + enabledValues: + - low + - medium + - high + - id: refresh + version: 1 + categories: + - name: Vent \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/elevator/init.lua b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua new file mode 100644 index 0000000000..51aa1d8e0e --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/elevator/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local elevator_cap = capabilities.elevatorCall +local onoff_cluster = clusters.OnOff + +local ELEVATOR_DEVICE_TYPE_ID = 0xFF02 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.called()) + else + device:emit_event_for_endpoint(ib.endpoint_id, elevator_cap.callStatus.standby()) + end +end + +local function handle_elevator_call(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.On(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, clusters.OnOff.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_elevator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == ELEVATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local elevator_handler = { + NAME = "Elevator Handler", + can_handle = is_matter_elevator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [elevator_cap.ID] = { + [elevator_cap.commands.call.NAME] = handle_elevator_call, + } + }, + supported_capabilities = { + elevator_cap, + }, + subscribed_attributes = { + [elevator_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return elevator_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua new file mode 100644 index 0000000000..52dd3cafb6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/gas-valve/init.lua @@ -0,0 +1,91 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local valve_cap = capabilities.safetyValve +local onoff_cluster = clusters.OnOff + +local GAS_VALVE_DEVICE_TYPE_ID = 0xFF01 + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, valve_cap.valve.closed()) + end +end + +local function handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = onoff_cluster.server.commands.Off(device, endpoint_id) + device:send(req) +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, ep in ipairs(eps) do + if ep ~= 0 then return ep end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + return find_default_endpoint(device, onoff_cluster.ID) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) +end + +local function info_changed(driver, device, event, args) + device:add_subscribed_attribute(onoff_cluster.attributes.OnOff) + device:subscribe() +end + +local function is_matter_gas_valve(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == GAS_VALVE_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local gas_valve_handler = { + NAME = "Gas Valve Handler", + can_handle = is_matter_gas_valve, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [onoff_cluster.ID] = { + [onoff_cluster.attributes.OnOff.ID] = on_off_attr_handler, + } + } + }, + capability_handlers = { + [valve_cap.ID] = { + [valve_cap.commands.close.NAME] = handle_valve_close, + } + }, + supported_capabilities = { + valve_cap, + }, + subscribed_attributes = { + [valve_cap.ID] = { + onoff_cluster.attributes.OnOff + } + }, +} + +return gas_valve_handler \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/init.lua b/drivers/SinuxSoft/britzyhub/src/init.lua new file mode 100644 index 0000000000..60f3cb90ce --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/init.lua @@ -0,0 +1,16 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local MatterDriver = require "st.matter.driver" +local log = require "log" + +local matter_driver = MatterDriver("britzyhub-matter", { + sub_drivers = { + require ("elevator"), + require ("gas-valve"), + require ("vent"), + } +}) + +log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) +matter_driver:run() \ No newline at end of file diff --git a/drivers/SinuxSoft/britzyhub/src/vent/init.lua b/drivers/SinuxSoft/britzyhub/src/vent/init.lua new file mode 100644 index 0000000000..93e11f40c6 --- /dev/null +++ b/drivers/SinuxSoft/britzyhub/src/vent/init.lua @@ -0,0 +1,164 @@ +-- SinuxSoft (c) 2025 +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local log = require "log" + +local VENTILATOR_DEVICE_TYPE_ID = 0xFF03 + +local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" + +local subscribed_attributes = { + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, +} + +local function map_enum_value(map, value, default) + return map[value] or default +end + +local function find_default_endpoint(device, cluster) + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then + return v + end + end + log.warn(string.format("No endpoint found, using default %d", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +local function component_to_endpoint(device, component_name, cluster_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return find_default_endpoint(device, cluster_id) +end + +local endpoint_to_component = function(device, endpoint_id) + local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +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.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +local AC_FAN_MODE_MAP = { + [clusters.FanControl.attributes.FanMode.LOW] = capabilities.fanMode.fanMode.low(), + [clusters.FanControl.attributes.FanMode.MEDIUM] = capabilities.fanMode.fanMode.medium(), + [clusters.FanControl.attributes.FanMode.HIGH] = capabilities.fanMode.fanMode.high(), +} + +local function fan_mode_handler(driver, device, ib, response) + local cap = capabilities.fanMode + if device:supports_capability_by_id(cap.ID) then + local event = map_enum_value(AC_FAN_MODE_MAP, ib.data.value, cap.fanMode.low()) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end +end + +local function handle_switch_on(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.On(device, ep)) +end + +local function handle_switch_off(driver, device, cmd) + local ep = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + device:send(clusters.OnOff.server.commands.Off(device, ep)) +end + +local function set_fan_mode(driver, device, cmd) + local args = cmd.args.fanMode + local mode_map = { + low = clusters.FanControl.attributes.FanMode.LOW, + medium = clusters.FanControl.attributes.FanMode.MEDIUM, + high = clusters.FanControl.attributes.FanMode.HIGH, + } + local ep = component_to_endpoint(device, cmd.component, clusters.FanControl.ID) + local mode = mode_map[args] or clusters.FanControl.attributes.FanMode.OFF + device:send(clusters.FanControl.attributes.FanMode:write(device, ep, mode)) +end + +local function device_init(driver, device) + device:subscribe() + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local function info_changed(driver, device, event, args) + 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:subscribe() +end + +local function is_matter_ventilator(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == VENTILATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +local ventilator_handler = { + NAME = "Ventilator Handler", + can_handle = is_matter_ventilator, + lifecycle_handlers = { + init = device_init, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, + }, + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_switch_on, + [capabilities.switch.commands.off.NAME] = handle_switch_off, + }, + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode, + }, + }, + supported_capabilities = { + capabilities.switch, + capabilities.fanMode, + }, + subscribed_attributes = subscribed_attributes +} + +return ventilator_handler \ No newline at end of file From 7c83c6ac25c46fd64d092455c02c7ded9aed017d Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Tue, 16 Sep 2025 08:24:40 +0900 Subject: [PATCH 125/449] Updating matter rvc ux (#2343) Signed-off-by: HunsupJung --- .../profiles/rvc-clean-mode-service-area.yml | 61 +-- .../matter-rvc/profiles/rvc-clean-mode.yml | 59 +-- .../matter-rvc/profiles/rvc-service-area.yml | 72 ---- .../SmartThings/matter-rvc/profiles/rvc.yml | 69 ---- drivers/SmartThings/matter-rvc/src/init.lua | 201 ++++------ .../matter-rvc/src/test/test_matter_rvc.lua | 348 +----------------- 6 files changed, 85 insertions(+), 725 deletions(-) diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml index 15c766fbdb..a2e064c04f 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -5,6 +5,8 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: serviceArea version: 1 - id: refresh @@ -13,20 +15,6 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner deviceConfig: dashboard: states: @@ -38,22 +26,15 @@ deviceConfig: capability: robotCleanerOperatingState version: 1 - component: main - capability: serviceArea - version: 1 - - component: runMode capability: mode version: 1 patch: - op: replace path: /0/list/command/supportedValues value: supportedArguments.value - - component: cleanMode - capability: mode + - component: main + capability: serviceArea version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - component: main capability: refresh version: 1 @@ -65,22 +46,7 @@ deviceConfig: - component: main capability: robotCleanerOperatingState version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode + - component: main capability: mode version: 1 patch: @@ -99,22 +65,7 @@ deviceConfig: - component: main capability: robotCleanerOperatingState version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode + - component: main capability: mode version: 1 patch: diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml index a83c7801a0..22347086d6 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml @@ -5,26 +5,14 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: firmwareUpdate version: 1 - id: refresh version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner deviceConfig: dashboard: states: @@ -35,14 +23,7 @@ deviceConfig: - component: main capability: robotCleanerOperatingState version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode + - component: main capability: mode version: 1 patch: @@ -60,22 +41,7 @@ deviceConfig: - component: main capability: robotCleanerOperatingState version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode + - component: main capability: mode version: 1 patch: @@ -94,22 +60,7 @@ deviceConfig: - component: main capability: robotCleanerOperatingState version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode + - component: main capability: mode version: 1 patch: diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml index 8f5ca7bc21..560eda12fa 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml @@ -13,75 +13,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc.yml b/drivers/SmartThings/matter-rvc/profiles/rvc.yml index 7246590f4e..84bed73495 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc.yml @@ -11,72 +11,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 4165535fef..ffd8af0ac0 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -32,10 +32,11 @@ if version.api < 13 then clusters.Global = require "Global" end -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" +local CURRENT_RUN_MODE = "__current_run_mode" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" local OPERATING_STATE_SUPPORTED_COMMANDS = "__operating_state_supported_commands" +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local subscribed_attributes = { [capabilities.mode.ID] = { @@ -54,32 +55,19 @@ local subscribed_attributes = { } } -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - else - return device.MATTER_DEFAULT_ENDPOINT +local function find_default_endpoint(device, cluster_id) + local eps = device:get_endpoints(cluster_id) + 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 device.MATTER_DEFAULT_ENDPOINT end -local function device_added(driver, device) - local run_mode_eps = device:get_endpoints(clusters.RvcRunMode.ID) or {} - local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} - local component_to_endpoint_map = { - ["main"] = run_mode_eps[1], - ["runMode"] = run_mode_eps[1], - ["cleanMode"] = clean_mode_eps[1] - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) -end - -local function device_init(driver, device) - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) -end - -local function do_configure(driver, device) +local function match_profile(driver, device) local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} local service_area_eps = embedded_cluster_utils.get_endpoints(device, clusters.ServiceArea.ID) or {} @@ -93,6 +81,25 @@ local function do_configure(driver, device) device.log.info_with({hub_logs = true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) +end + +local function device_init(driver, device) + device:subscribe() + + -- comp/ep map functionality removed 9/5/25. + device:set_field("__component_to_endpoint_map", nil) + + if not device:get_field(SERVICE_AREA_PROFILED) then + if #device:get_endpoints(clusters.ServiceArea.ID) > 0 then + match_profile(driver, device) + end + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) end @@ -159,7 +166,7 @@ local function can_send_state_command(device, command_name, current_state, curre return false end -local function update_supported_arguments(device, current_run_mode, current_state) +local function update_supported_arguments(device, ep, current_run_mode, current_state) device.log.info(string.format("update_supported_arguments: %s, %s", current_run_mode, current_state)) if current_run_mode == nil or current_state == nil then return @@ -170,15 +177,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( {}, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - -- Set runMode to empty - event = capabilities.mode.supportedArguments({}, {visibility = {displayed = false}}) - device:emit_component_event(device.profile.components["runMode"], event) - -- Set cleanMode to empty - local component = device.profile.components["cleanMode"] - if component ~= nil then - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) return end @@ -198,7 +197,6 @@ local function update_supported_arguments(device, current_run_mode, current_stat -- Set Supported Operating State Commands local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState local supported_op_commands = {} if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, nil) == true then @@ -213,42 +211,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - - -- Check whether non-idle mode can be selected or not - local can_be_non_idle = false - if current_tag == clusters.RvcRunMode.types.ModeTag.IDLE and - (current_state == cap_op_enum.stopped.NAME or current_state == cap_op_enum.paused.NAME or - current_state == cap_op_enum.docked.NAME or current_state == cap_op_enum.charging.NAME) then - can_be_non_idle = true - end - - -- Set supported run arguments - local supported_arguments = {} -- For generic plugin - for _, mode in ipairs(supported_run_modes) do - if mode.tag == clusters.RvcRunMode.types.ModeTag.IDLE or can_be_non_idle == true then - table.insert(supported_arguments, mode.label) - end - end - - -- Send event to set supported run arguments - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - - -- Set supported clean arguments - local supported_clean_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - supported_arguments = {} - for _, mode in ipairs(supported_clean_modes) do - table.insert(supported_arguments, mode.label) - end - - -- Send event to set supported clean modes - local component = device.profile.components["cleanMode"] - if component ~= nil then - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) end -- Matter Handlers -- @@ -279,23 +242,14 @@ local function run_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(RUN_MODE_SUPPORTED_MODES, supported_modes_id_tag, { persist = true }) - -- Update Supported Modes - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - -- Update Supported Arguments - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_state = device:get_latest_state( "main", capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function run_mode_current_mode_handler(driver, device, ib, response) @@ -315,8 +269,7 @@ local function run_mode_current_mode_handler(driver, device, ib, response) end -- Set current mode - local component = device.profile.components["runMode"] - device:emit_component_event(component, capabilities.mode.mode(current_run_mode)) + device:set_field(CURRENT_RUN_MODE, current_run_mode, { persist = true }) -- Update supported mode local current_state = device:get_latest_state( @@ -324,11 +277,10 @@ local function run_mode_current_mode_handler(driver, device, ib, response) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function clean_mode_supported_mode_handler(driver, device, ib, response) - device.log.info("clean_mode_supported_mode_handler") local supported_modes = {} local supported_modes_id = {} for _, mode in ipairs(ib.data.elements) do @@ -340,11 +292,10 @@ local function clean_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(CLEAN_MODE_SUPPORTED_MODES, supported_modes_id, { persist = true }) - local component = device.profile.components["cleanMode"] local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function clean_mode_current_mode_handler(driver, device, ib, response) @@ -353,8 +304,7 @@ local function clean_mode_current_mode_handler(driver, device, ib, response) local supported_clean_mode = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_clean_mode) do if mode.id == mode_id then - local component = device.profile.components["cleanMode"] - device:emit_component_event(component, capabilities.mode.mode(mode.label)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode.label)) break end end @@ -378,15 +328,11 @@ local function rvc_operational_state_attr_handler(driver, device, ib, response) end -- Supported Mode update - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) if ib.data.value ~= clus_op_enum.ERROR then - update_supported_arguments(device, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) else - update_supported_arguments(device, current_run_mode, "Error") + update_supported_arguments(device, ib.endpoint_id, current_run_mode, "Error") end end @@ -438,11 +384,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device device:set_field(OPERATING_STATE_SUPPORTED_COMMANDS, supportedOperatingStateCommands, { persist = true }) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -478,7 +420,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function upper_to_camelcase(name) @@ -523,9 +465,8 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp end -- Update Supported Areas - local component = device.profile.components["main"] local event = capabilities.serviceArea.supportedAreas(supported_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end -- In case selected area is not in supportedarea then should i add to supported area or remove from selectedarea @@ -535,9 +476,8 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo table.insert(selected_areas, areaId.value) end - local component = device.profile.components["main"] local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function robot_cleaner_areas_selection_response_handler(driver, device, ib, response) @@ -552,23 +492,18 @@ local function robot_cleaner_areas_selection_response_handler(driver, device, ib else device.log.error(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) local selectedAreas = device:get_latest_state("main", capabilities.serviceArea.ID, capabilities.serviceArea.selectedAreas.NAME) - local component = device.profile.components["main"] local event = capabilities.serviceArea.selectedAreas(selectedAreas, {state_change = true}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end end -- Capability Handlers -- local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_start") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -594,7 +529,7 @@ local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device:send(clusters.RvcOperationalState.commands.Resume(device, endpoint_id)) elseif can_send_state_command(device, capabilities.mode.commands.setMode.NAME, current_state, current_tag) == true then for _, mode in ipairs(supported_run_modes) do - endpoint_id = device:component_to_endpoint("runMode") + endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if mode.tag == clusters.RvcRunMode.types.ModeTag.CLEANING then device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) return @@ -605,37 +540,26 @@ end local function handle_robot_cleaner_operating_state_pause(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_pause") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.Pause(device, endpoint_id)) end local function handle_robot_cleaner_operating_state_go_home(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_go_home") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.GoHome(device, endpoint_id)) end local function handle_robot_cleaner_mode(driver, device, cmd) device.log.info(string.format("handle_robot_cleaner_mode component: %s, mode: %s", cmd.component, cmd.args.mode)) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "runMode" then - local supported_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end - end - elseif cmd.component == "cleanMode" then - local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) + local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} + for _, mode in ipairs(supported_modes) do + if cmd.args.mode == mode.label then + device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) + device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) + return end end end @@ -648,7 +572,7 @@ local function handle_robot_cleaner_areas_selection(driver, device, cmd) for i, areaId in ipairs(cmd.args.areas) do table.insert(selectAreas, uint32_dt(areaId)) end - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if cmd.component == "main" then device:send(clusters.ServiceArea.commands.SelectAreas(device, endpoint_id, selectAreas)) end @@ -657,7 +581,6 @@ end local matter_rvc_driver = { lifecycle_handlers = { init = device_init, - added = device_added, doConfigure = do_configure, infoChanged = info_changed, }, 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 5bed191802..c051f188fb 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -31,6 +31,7 @@ if version.api < 13 then end local APPLICATION_ENDPOINT = 10 +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rvc-clean-mode-service-area.yml"), @@ -64,6 +65,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + mock_device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) test.disable_startup_messages() test.mock_device.add_test_device(mock_device) local subscribed_attributes = { @@ -119,8 +121,6 @@ local RUN_MODES = { CLEANING_MODE, } -local RUN_MODE_LABELS = { RUN_MODES[1].label, RUN_MODES[2].label, RUN_MODES[3].label } - local CLEAN_MODE_1 = { label = "Clean Mode 1", mode = 0, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 1 }) } } local CLEAN_MODE_2 = { label = "Clean Mode 2", mode = 1, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 2 }) } } @@ -143,12 +143,6 @@ local function supported_run_mode_init() } ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedModes(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end local function supported_clean_mode_init() @@ -163,13 +157,13 @@ local function supported_clean_mode_init() }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedModes(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) @@ -223,7 +217,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On changing the run mode to a mode with an IDLE tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an IDLE tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -235,12 +229,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -253,23 +241,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an CLEANING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an CLEANING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -281,12 +257,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -296,23 +266,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an MAPPING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an MAPPING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -324,12 +282,6 @@ test.register_coroutine_test( MAPPING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = MAPPING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -339,18 +291,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -370,7 +310,7 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.mode({value = cleanMode.label}) ) ) @@ -378,24 +318,6 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "On changing the rvc run mode, appropriate RvcRunMode command must be sent to the device", function() - supported_run_mode_init() - operating_state_init() - test.wait_for_events() - for _, runMode in ipairs(RUN_MODES) do - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "mode", component = "runMode", command = "setMode", args = { runMode.label } } - }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.RvcRunMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, runMode.mode) - }) - end - end -) - test.register_coroutine_test( "On changing the rvc clean mode, appropriate RvcCleanMode command must be sent to the device", function() supported_clean_mode_init() @@ -404,7 +326,7 @@ test.register_coroutine_test( for _, cleanMode in ipairs(CLEAN_MODES) do test.socket.capability:__queue_receive({ mock_device.id, - { capability = "mode", component = "cleanMode", command = "setMode", args = { cleanMode.label } } + { capability = "mode", component = "main", command = "setMode", args = { cleanMode.label } } }) test.socket.matter:__expect_send({ mock_device.id, @@ -415,7 +337,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the Start Command, supportedArgument must be set to the appropriate value", function() + "On receive the start Command of the capability, ChangeToMode command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -428,12 +350,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -446,18 +362,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -471,7 +375,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the goHome Command, supportedArgument must be set to the appropriate value", function() + "On receive the goHome Command of the capability, GoHome command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -484,12 +388,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -499,18 +397,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -524,7 +410,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the pause Command, supportedArgument must be set to the appropriate value", function() + "On receive the pause Command of the capability, Pause command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -537,12 +423,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -552,18 +432,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -590,18 +458,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -627,12 +483,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -645,18 +495,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -680,18 +518,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -708,12 +534,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -726,18 +546,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -764,18 +572,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -792,12 +588,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -810,18 +600,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -848,18 +626,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -876,12 +642,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -894,18 +654,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -929,18 +677,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -957,12 +693,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -975,18 +705,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1010,18 +728,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -1038,12 +744,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1056,18 +756,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1085,18 +773,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalError:build_test_report_data( From 1f0a05af6f456a85ec5f0774e4cde9e00058eb63 Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Wed, 17 Sep 2025 02:19:17 +0900 Subject: [PATCH 126/449] Update matter-rvc driver (#2400) - Replace Embedded device configuration with Device presentation to support translation - If selected_area is empty, selecting all areas Signed-off-by: HunsupJung --- .../profiles/rvc-clean-mode-service-area.yml | 68 +------------------ .../matter-rvc/profiles/rvc-clean-mode.yml | 65 +----------------- drivers/SmartThings/matter-rvc/src/init.lua | 11 +++ 3 files changed, 17 insertions(+), 127 deletions(-) diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml index a2e064c04f..c15783f6d2 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -15,68 +15,6 @@ components: version: 1 categories: - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: serviceArea - version: 1 - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml index 22347086d6..33af113030 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml @@ -13,65 +13,6 @@ components: version: 1 categories: - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index ffd8af0ac0..25a202241a 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -476,6 +476,17 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo table.insert(selected_areas, areaId.value) end + if next(selected_areas) == nil then + local supported_areas = device:get_latest_state( + "main", + capabilities.serviceArea.ID, + capabilities.serviceArea.supportedAreas.NAME + ) + for i, area in ipairs(supported_areas) do + table.insert(selected_areas, area.areaId) + end + end + local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) device:emit_event_for_endpoint(ib.endpoint_id, event) end From c1bae070f4540a7e0d555184875f907f5e29765a Mon Sep 17 00:00:00 2001 From: seojune Date: Wed, 17 Sep 2025 09:41:42 +0900 Subject: [PATCH 127/449] Added BatteryLevel Capability --- .../aqara-lock/profiles/aqara-lock-battery.yml | 2 ++ drivers/Aqara/aqara-lock/src/init.lua | 17 +++++++++++++++++ .../aqara-lock/src/test/test_aqara_lock.lua | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml b/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml index 0af3282ddb..0fbe7a718b 100644 --- a/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml +++ b/drivers/Aqara/aqara-lock/profiles/aqara-lock-battery.yml @@ -6,6 +6,8 @@ components: version: 1 - id: battery version: 1 + - id: batteryLevel + version: 1 - id: lockAlarm version: 1 - id: remoteControlStatus diff --git a/drivers/Aqara/aqara-lock/src/init.lua b/drivers/Aqara/aqara-lock/src/init.lua index 0d7cc77c4f..e9eb32c9b4 100644 --- a/drivers/Aqara/aqara-lock/src/init.lua +++ b/drivers/Aqara/aqara-lock/src/init.lua @@ -25,6 +25,10 @@ local SUPPORTED_ALARM_VALUES = { "damaged", "forcedOpeningAttempt", "unableToLoc local SERIAL_NUM_TX = "serial_num_tx" local SERIAL_NUM_RX = "serial_num_rx" local SEQ_NUM = "seq_num" +local THRESHOLD_BATTERY = { + ["aqara.lock.akr011"] = { low = 47, dryout = 28 }, -- K100 + ["aqara.lock.akr001"] = { low = 47, dryout = 31 } -- L100 +} local function my_secret_data_handler(driver, device, secret_info) if secret_info.secret_kind ~= "aqara" then return end @@ -75,16 +79,19 @@ local function device_init(self, device) device:emit_event(capabilities.lock.supportedUnlockDirections({ "fromInside", "fromOutside" }, { visibility = { displayed = false } })) device:emit_event(capabilities.battery.type("AA")) + device:emit_event(capabilities.batteryLevel.type("AA")) local battery_quantity = 8 if device:get_model() == "aqara.lock.akr001" then battery_quantity = 6 end device:emit_event(capabilities.battery.quantity(battery_quantity)) + device:emit_event(capabilities.batteryLevel.quantity(battery_quantity)) end local function device_added(self, device) remoteControlShow(device) device:emit_event(Battery.battery(100)) + device:emit_event(capabilities.batteryLevel.battery("normal")) device:emit_event(LockAlarm.alarm.clear({ visibility = { displayed = false } })) device:emit_event(antiLockStatus.antiLockStatus("unknown", { visibility = { displayed = false } })) device:emit_event(Lock.lock.locked()) @@ -158,8 +165,18 @@ local function event_door_handler(driver, device, evt_name, evt_value) end end +local function calc_battery_level(model, level) + local batteryLevel = "normal" + if level < THRESHOLD_BATTERY[model].dryout then + batteryLevel = "critical" + elseif level < THRESHOLD_BATTERY[model].low then + batteryLevel = "warning" + end + return batteryLevel +end local function event_battery_handler(driver, device, evt_name, evt_value) device:emit_event(Battery.battery(evt_value)) + device:emit_event(capabilities.batteryLevel.battery(calc_battery_level(device:get_model(), evt_value))) end local function event_abnormal_status_handler(driver, device, evt_name, evt_value) diff --git a/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua b/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua index f127a16d0c..8fcec06b5f 100644 --- a/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua +++ b/drivers/Aqara/aqara-lock/src/test/test_aqara_lock.lua @@ -13,6 +13,7 @@ test.add_package_capability("lockCredentialInfo.yaml") local lockAlarm = capabilities["lockAlarm"] test.add_package_capability("lockAlarm.yaml") local Battery = capabilities.battery +local BatteryLevel = capabilities.batteryLevel local Lock = capabilities.lock local PRI_CLU = 0xFCC0 @@ -45,7 +46,9 @@ local function test_init() test.socket.capability:__expect_send(mock_device:generate_test_message("main", Lock.supportedUnlockDirections({"fromInside", "fromOutside"}, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.type("AA"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.type("AA"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.quantity(8))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.quantity(8))) test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -59,6 +62,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", remoteControlStatus.remoteControlEnabled('false', { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.battery(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", lockAlarm.alarm.clear({ visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", @@ -76,6 +80,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", remoteControlStatus.remoteControlEnabled('true', { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", Battery.battery(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", BatteryLevel.battery("normal"))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", lockAlarm.alarm.clear({ visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", From df2fc59cd020d90867ce856af8563fb1f31e0bb9 Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Wed, 17 Sep 2025 21:31:03 +0900 Subject: [PATCH 128/449] Update new-matter-lock driver to support Aliro feature (#2344) Signed-off-by: Hunsup Jung Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --- .../lock-modular-embedded-unlatch.yml | 3 + .../matter-lock/profiles/lock-modular.yml | 3 + .../matter-lock/src/lock_utils.lua | 12 +- .../matter-lock/src/new-matter-lock/init.lua | 1368 +++++++++++++---- .../src/test/test_new_matter_lock.lua | 23 +- 5 files changed, 1079 insertions(+), 330 deletions(-) diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml index 4ea6ba1e0d..3d9e68b44c 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml index 576695f873..3a8a53bf70 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 5d92c55afa..816ca446f2 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -40,7 +40,17 @@ local lock_utils = { SCHEDULE_END_HOUR = "scheduleEndHour", SCHEDULE_END_MINUTE = "scheduleEndMinute", SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", - SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime", + VERIFICATION_KEY = "verificationKey", + GROUP_ID = "groupId", + GROUP_RESOLVING_KEY = "groupResolvingKey", + ISSUER_KEY = "issuerKey", + ISSUER_KEY_INDEX = "issuerKeyIndex", + ENDPOINT_KEY = "endpointKey", + ENDPOINT_KEY_INDEX = "endpointKeyIndex", + ENDPOINT_KEY_TYPE = "endpointKeyType", + DEVICE_KEY_ID = "deviceKeyId", + COMMAND_REQUEST_ID = "commandRequestId" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index aad7046a3d..f9c06f193c 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -27,12 +27,37 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local INITIAL_COTA_INDEX = 1 +local INITIAL_CREDENTIAL_INDEX = 1 local ALL_INDEX = 0xFFFE 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 NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 @@ -86,6 +111,17 @@ local subscribed_attributes = { 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 }, @@ -193,6 +229,9 @@ local function match_profile_modular(driver, device) 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 @@ -295,7 +334,7 @@ local function driver_switched(driver, device) end -- This function check busy_state and if busy_state is false, set it to true(current time) -local function check_busy_state(device) +local function is_busy_state_set(device) local c_time = os.time() local busy_state = device:get_field(lock_utils.BUSY_STATE) or false if busy_state == false or c_time - busy_state > 10 then @@ -404,7 +443,7 @@ local function set_cota_credential(device, credential_index) end -- Check Busy State - if check_busy_state(device) == true then + if 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) @@ -451,7 +490,7 @@ local function apply_cota_credentials_if_absent(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) + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) end) end) end @@ -479,6 +518,113 @@ 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 Util -- +---------------- +local function hex_string_to_octet_string(hex_string) + if hex_string == nil then + return nil + end + local octet_string = "" + for i = 1, #hex_string, 2 do + local hex = hex_string:sub(i, i + 1) + octet_string = octet_string .. string.char(tonumber(hex, 16)) + end + return octet_string +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}} + )) + 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 + --------------------------------- -- Power Source Attribute List -- --------------------------------- @@ -579,7 +725,7 @@ end ---------------- -- User Table -- ---------------- -local function add_user_to_table(device, userIdx, usrType) +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", @@ -589,11 +735,11 @@ local function add_user_to_table(device, userIdx, usrType) )) -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userType = usrType}) + 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, usrType) +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", @@ -613,7 +759,8 @@ local function update_user_in_table(device, userIdx, usrType) -- Update user entry if i ~= 0 then - user_table[i].userType = usrType + user_table[i].userType = userType + user_table[i].userName = userName device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end end @@ -646,6 +793,40 @@ 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( @@ -664,7 +845,7 @@ 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 + return ALL_INDEX end -- Get latest credential table @@ -676,7 +857,7 @@ local function delete_credential_from_table(device, credIdx) )) -- Delete an entry from credential table - local userIdx = 0 + local userIdx = nil for index, entry in pairs(cred_table) do if entry.credentialIndex == credIdx then table.remove(cred_table, index) @@ -717,16 +898,6 @@ end ----------------------------- -- Week Day Schedule Table -- ----------------------------- -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - 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( @@ -980,6 +1151,76 @@ local function delete_year_schedule_from_table_as_user(device, userIdx) 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 -- -------------- @@ -990,32 +1231,26 @@ local function handle_add_user(driver, device, command) local userType = command.args.userType -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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_COTA_INDEX, {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_COTA_INDEX)) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) end ----------------- @@ -1026,33 +1261,28 @@ local function handle_update_user(driver, device, command) local cmdName = "updateUser" local userIdx = command.args.userIndex local userName = command.args.userName - local userType = command.args.lockUserType + 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 - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1086,19 +1316,14 @@ local function get_user_response_handler(driver, device, ib, response) end if status ~= "success" then -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1137,19 +1362,14 @@ local function get_user_response_handler(driver, device, ib, response) ) elseif userIdx >= maxUser then -- There's no available user index -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = "resourceExhausted" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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)) @@ -1163,6 +1383,7 @@ 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 @@ -1176,28 +1397,23 @@ local function set_user_response_handler(driver, device, ib, response) -- Update User in table if status == "success" then if cmdName == "addUser" then - add_user_to_table(device, userIdx, userType) + add_user_to_table(device, userIdx, userName, userType) elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userType) + 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 result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1210,20 +1426,14 @@ local function handle_delete_user(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1244,20 +1454,14 @@ local function handle_delete_all_users(driver, device, command) local cmdName = "deleteAllUsers" -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1288,6 +1492,7 @@ local function clear_user_response_handler(driver, device, ib, response) 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 @@ -1295,18 +1500,14 @@ local function clear_user_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - }) - device:emit_event(event) + 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 @@ -1327,25 +1528,19 @@ local function handle_add_credential(driver, device, command) end local credential = { credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_COTA_INDEX + credential_index = INITIAL_CREDENTIAL_INDEX } local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1353,7 +1548,7 @@ local function handle_add_credential(driver, device, command) 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_COTA_INDEX, {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 @@ -1386,20 +1581,14 @@ local function handle_update_credential(driver, device, command) local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1423,19 +1612,10 @@ local function handle_update_credential(driver, device, command) ) end ------------------------------ --- Set Credential Response -- ------------------------------ -local RESPONSE_STATUS_MAP = { - [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 function set_credential_response_handler(driver, device, ib, response) +--------------------------------- +-- 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 @@ -1449,9 +1629,10 @@ local function set_credential_response_handler(driver, device, ib, response) 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 status = "success" local elements = ib.info_block.data.elements - if elements.status.value == DoorLock.types.DlStatus.SUCCESS then + 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}) @@ -1460,7 +1641,7 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - add_user_to_table(device, elements.user_index.value, userType) + add_user_to_table(device, elements.user_index.value, nil, userType) end -- Update Credential table @@ -1470,20 +1651,15 @@ local function set_credential_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, credentialIndex = credIdx, statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) -- 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}) @@ -1513,33 +1689,20 @@ local function set_credential_response_handler(driver, device, ib, response) return end - -- Update commandResult - status = RESPONSE_STATUS_MAP[elements.status.value] + -- 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)) - - -- Set commandResult to error status - if 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) - return - elseif status ~= "occupied" then - local result = { + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { commandName = cmdName, - statusCode = status + statusCode = "resourceExhausted" -- No more available credential index } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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}) - return - end - - if elements.next_credential_index.value ~= nil then + elseif status == "occupied" then -- Get parameters local credIdx = elements.next_credential_index.value local credential = { @@ -1572,60 +1735,264 @@ local function set_credential_response_handler(driver, device, ib, response) 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 result = { + local command_result_info = { commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 ------------------------ --- 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, - } +----------------------------------- +-- 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] - -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + 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, - statusCode = "busy" + userIndex = userIdx, + requestId = reqId, + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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.CRED_INDEX, credIdx, {persist = true}) + -- 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 + } - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + -- 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 + 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 + 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 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 ---------------------------- @@ -1640,20 +2007,14 @@ local function handle_delete_all_credentials(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1670,42 +2031,71 @@ end -- Clear Credential Response -- ------------------------------- local function clear_credential_response_handler(driver, device, ib, response) - -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credIdx = device:get_field(lock_utils.CRED_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" + 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 - -- Delete User in table - local userIdx = 0 - if status == "success" then + 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 == 0 then - userIdx = nil + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) end - else - device.log.warn(string.format("Failed to clear credential: %s", status)) + + -- 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 - local result = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1730,20 +2120,14 @@ local function handle_set_week_day_schedule(driver, device, command) local endMinute = schedule.endMinute -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1808,20 +2192,15 @@ local function set_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1835,20 +2214,14 @@ local function handle_clear_week_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1885,20 +2258,15 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1942,20 +2310,14 @@ local function handle_set_year_day_schedule(driver, device, command) local localEndTime = command.args.schedule.localEndTime -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2011,20 +2373,15 @@ local function set_year_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -2038,20 +2395,14 @@ local function handle_clear_year_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2160,6 +2511,354 @@ local function handle_refresh(driver, device, command) 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 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, 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, + hex_string_to_octet_string(signingKey), + hex_string_to_octet_string(verificationKey), + hex_string_to_octet_string(groupId), -- Group identification + hex_string_to_octet_string(groupResolvingKey) -- Group resolving key + ) + ) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local verificationKey = device:get_field(lock_utils.VERIFICATION_KEY) + local groupId = device:get_field(lock_utils.GROUP_ID) + local groupResolvingKey = device:get_field(lock_utils.GROUP_RESOLVING_KEY) + + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + elseif ib.status == DoorLock.types.DlStatus.SUCCESS then + if verificationKey ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + verificationKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupId ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + groupId, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupResolvingKey ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + groupResolvingKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + 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 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 + 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 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 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 + 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 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 = { @@ -2181,6 +2880,14 @@ local new_matter_lock_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, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, @@ -2204,6 +2911,7 @@ local new_matter_lock_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, }, }, }, @@ -2233,6 +2941,14 @@ local new_matter_lock_handler = { [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 = { 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 6067d056ff..ab9ac68c88 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 @@ -971,7 +971,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users({{userIndex = 1, userType = "adminMember"}}, {visibility={displayed=false}}) + capabilities.lockUsers.users({{userIndex = 1, userName="Guest1", userType = "adminMember"}}, {visibility={displayed=false}}) ) ) test.socket.capability:__expect_send( @@ -1493,7 +1493,6 @@ test.register_coroutine_test( ), } ) - -- test.wait_for_events() end ) @@ -1745,11 +1744,29 @@ test.register_coroutine_test( 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.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - {commandName="deleteAllCredentials", credentialIndex=65534, statusCode="success"}, + {commandName="deleteAllCredentials", userIndex=65534, credentialIndex=65534, statusCode="success"}, {state_change=true, visibility={displayed=false}} ) ) From dad0368c8caa36c4cd43968647b47168ade68c06 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 16 Sep 2025 14:54:01 -0500 Subject: [PATCH 129/449] sonos: Fix nil index error in get_groups response --- drivers/SmartThings/sonos/src/sonos_state.lua | 100 +++++++++++------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index fb316f8625..12d10f6211 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -11,7 +11,7 @@ local SonosConnection = require "api.sonos_connection" --- Information on an entire Sonos system ("household"), such as its current groups, list of players, etc. --- @field public id HouseholdId --- @field public groups table All of the current groups in the system ---- @field public players table All of the current players in the system +--- @field public players table All of the current players in the system --- @field public bonded_players table PlayerID's in this map that map to true are non-primary bonded players, and not controllable. --- @field public player_to_group table quick lookup from Player ID -> Group ID --- @field public st_devices table Player ID -> ST Device Record UUID information for the household @@ -77,7 +77,7 @@ end local _STATE = { ---@type Households households = make_households_table(), - ---@type table + ---@type table device_record_map = {}, } @@ -91,6 +91,7 @@ SonosState.__index = SonosState function SonosState:associate_device_record(device, info) local household_id = info.ssdp_info.household_id local group_id = info.ssdp_info.group_id + -- This is the device id even if the device is a secondary in a bonded set local player_id = info.discovery_info.playerId local household = _STATE.households[household_id] @@ -120,9 +121,8 @@ function SonosState:associate_device_record(device, info) end local player = (household.players[player_id] or {}).player - local sonos_device = (household.players[player_id] or {}).device - if not (player and sonos_device) then + if not player then log.error( string.format( "No record of Sonos player for device %s", @@ -132,12 +132,12 @@ function SonosState:associate_device_record(device, info) return end - household.st_devices[sonos_device.id] = device.id + household.st_devices[player_id] = device.id _STATE.device_record_map[device.id] = - { sonos_device = sonos_device, group = group, player = player, household = household } + { sonos_device_id = player_id, group = group, player = player, household = household } - local bonded = household.bonded_players[sonos_device.id or {}] and true or false + local bonded = household.bonded_players[player_id] and true or false local sw_gen_changed = utils.update_field_if_changed( device, @@ -179,7 +179,7 @@ function SonosState:associate_device_record(device, info) local player_id_changed = utils.update_field_if_changed( device, PlayerFields.PLAYER_ID, - sonos_device.id, + player_id, { persist = true } ) @@ -306,6 +306,40 @@ function SonosState:update_device_record_from_state(household_id, device) self:update_device_record_group_info(household, current_mapping.group, device) end +-- Helper function for when updating household info +local function update_device_info(driver, player, household, known_bonded_players, sonos_device_id) + household.players[sonos_device_id] = { player = player } + local previously_bonded = known_bonded_players[sonos_device_id] and true or false + local currently_bonded + local group_id + + -- The primary bonded player will have the same id as the top level player id + if sonos_device_id == player.id then + currently_bonded = false + else + currently_bonded = true + end + group_id = household.player_to_group[player.id] + household.player_to_group[sonos_device_id] = group_id + household.bonded_players[sonos_device_id] = currently_bonded + + local maybe_device_id = household.st_devices[sonos_device_id] + if maybe_device_id then + _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} + _STATE.device_record_map[maybe_device_id].household = household + _STATE.device_record_map[maybe_device_id].group = household.groups[group_id] + _STATE.device_record_map[maybe_device_id].player = player + _STATE.device_record_map[maybe_device_id].sonos_device_id = sonos_device_id + if previously_bonded ~= currently_bonded then + local target_device = driver:get_device_info(maybe_device_id) + if target_device then + target_device:set_field(PlayerFields.BONDED, currently_bonded, { persist = false }) + driver:update_bonded_device_tracking(target_device) + end + end + end +end + --- @param id HouseholdId --- @param groups_event SonosGroupsResponseBody --- @param driver SonosDriver @@ -323,41 +357,29 @@ function SonosState:update_household_info(id, groups_event, driver) end end + -- Iterate through the players and track all the devices associated with them + -- for bonded set tracking. + local log_devices_error = false for _, player in ipairs(players) do - for _, device in ipairs(player.devices) do - household.players[device.id] = { player = player, device = device } - local previously_bonded = known_bonded_players[device.id] and true or false - local currently_bonded - local group_id - -- non-primary bonded players are excluded from a group's list of PlayerID's so we use the group membership - -- of the primary device - if type(device.primaryDeviceId) == "string" and device.primaryDeviceId ~= "" then - currently_bonded = true - group_id = household.player_to_group[device.primaryDeviceId] - else - currently_bonded = false - group_id = household.player_to_group[device.id] + -- Prefer devices because deviceIds is deprecated but all we care about is + -- the ID so either way is fine. + if player.devices then + for _, device in ipairs(player.devices) do + update_device_info(driver, player, household, known_bonded_players, device.id) end - household.player_to_group[device.id] = group_id - household.bonded_players[device.id] = currently_bonded - - local maybe_device_id = household.st_devices[device.id] - if maybe_device_id then - _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} - _STATE.device_record_map[maybe_device_id].household = household - _STATE.device_record_map[maybe_device_id].group = household.groups[group_id] - _STATE.device_record_map[maybe_device_id].player = player - _STATE.device_record_map[maybe_device_id].sonos_device = device - if previously_bonded ~= currently_bonded then - local target_device = driver:get_device_info(maybe_device_id) - if target_device then - target_device:set_field(PlayerFields.BONDED, currently_bonded, { persist = false }) - driver:update_bonded_device_tracking(target_device) - end - end + elseif player.deviceIds then + for _, device_id in ipairs(player.deviceIds) do + update_device_info(driver, player, household, known_bonded_players, device_id) end + else + log_devices_error = true + -- We can still track the primary player in this case + update_device_info(driver, player, household, known_bonded_players, player.id) end end + if log_devices_error then + log.warn_with( { hub_logs = true}, "Group event contained neither devices nor deviceIds in player") + end household.id = id _STATE.households[id] = household @@ -489,7 +511,7 @@ function SonosState:get_sonos_ids_for_device(device) -- player id *should* be stable if not player_id then - player_id = sonos_objects.sonos_device.id + player_id = sonos_objects.sonos_device_id device:set_field(PlayerFields.PLAYER_ID, player_id, { persist = true }) end From b6a52bc0d766604b4db10dcb5289569f53a6f269 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:43:54 -0500 Subject: [PATCH 130/449] Beta hotfix: matter rvc ux upgrade, matter lock aliro support (#2403) * Updating matter rvc ux (#2343) Signed-off-by: HunsupJung * Update matter-rvc driver (#2400) - Replace Embedded device configuration with Device presentation to support translation - If selected_area is empty, selecting all areas Signed-off-by: HunsupJung * Update new-matter-lock driver to support Aliro feature (#2344) Signed-off-by: Hunsup Jung Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --------- Signed-off-by: HunsupJung Signed-off-by: Hunsup Jung Co-authored-by: HunsupJung <59987061+HunsupJung@users.noreply.github.com> --- .../lock-modular-embedded-unlatch.yml | 3 + .../matter-lock/profiles/lock-modular.yml | 3 + .../matter-lock/src/lock_utils.lua | 12 +- .../matter-lock/src/new-matter-lock/init.lua | 1368 +++++++++++++---- .../src/test/test_new_matter_lock.lua | 23 +- .../profiles/rvc-clean-mode-service-area.yml | 121 +- .../matter-rvc/profiles/rvc-clean-mode.yml | 118 +- .../matter-rvc/profiles/rvc-service-area.yml | 72 - .../SmartThings/matter-rvc/profiles/rvc.yml | 69 - drivers/SmartThings/matter-rvc/src/init.lua | 212 +-- .../matter-rvc/src/test/test_matter_rvc.lua | 348 +---- 11 files changed, 1174 insertions(+), 1175 deletions(-) diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml index 4ea6ba1e0d..3d9e68b44c 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml index 576695f873..3a8a53bf70 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 5d92c55afa..816ca446f2 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -40,7 +40,17 @@ local lock_utils = { SCHEDULE_END_HOUR = "scheduleEndHour", SCHEDULE_END_MINUTE = "scheduleEndMinute", SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", - SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime", + VERIFICATION_KEY = "verificationKey", + GROUP_ID = "groupId", + GROUP_RESOLVING_KEY = "groupResolvingKey", + ISSUER_KEY = "issuerKey", + ISSUER_KEY_INDEX = "issuerKeyIndex", + ENDPOINT_KEY = "endpointKey", + ENDPOINT_KEY_INDEX = "endpointKeyIndex", + ENDPOINT_KEY_TYPE = "endpointKeyType", + DEVICE_KEY_ID = "deviceKeyId", + COMMAND_REQUEST_ID = "commandRequestId" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index aad7046a3d..f9c06f193c 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -27,12 +27,37 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local INITIAL_COTA_INDEX = 1 +local INITIAL_CREDENTIAL_INDEX = 1 local ALL_INDEX = 0xFFFE 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 NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 @@ -86,6 +111,17 @@ local subscribed_attributes = { 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 }, @@ -193,6 +229,9 @@ local function match_profile_modular(driver, device) 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 @@ -295,7 +334,7 @@ local function driver_switched(driver, device) end -- This function check busy_state and if busy_state is false, set it to true(current time) -local function check_busy_state(device) +local function is_busy_state_set(device) local c_time = os.time() local busy_state = device:get_field(lock_utils.BUSY_STATE) or false if busy_state == false or c_time - busy_state > 10 then @@ -404,7 +443,7 @@ local function set_cota_credential(device, credential_index) end -- Check Busy State - if check_busy_state(device) == true then + if 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) @@ -451,7 +490,7 @@ local function apply_cota_credentials_if_absent(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) + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) end) end) end @@ -479,6 +518,113 @@ 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 Util -- +---------------- +local function hex_string_to_octet_string(hex_string) + if hex_string == nil then + return nil + end + local octet_string = "" + for i = 1, #hex_string, 2 do + local hex = hex_string:sub(i, i + 1) + octet_string = octet_string .. string.char(tonumber(hex, 16)) + end + return octet_string +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}} + )) + 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 + --------------------------------- -- Power Source Attribute List -- --------------------------------- @@ -579,7 +725,7 @@ end ---------------- -- User Table -- ---------------- -local function add_user_to_table(device, userIdx, usrType) +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", @@ -589,11 +735,11 @@ local function add_user_to_table(device, userIdx, usrType) )) -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userType = usrType}) + 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, usrType) +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", @@ -613,7 +759,8 @@ local function update_user_in_table(device, userIdx, usrType) -- Update user entry if i ~= 0 then - user_table[i].userType = usrType + user_table[i].userType = userType + user_table[i].userName = userName device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end end @@ -646,6 +793,40 @@ 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( @@ -664,7 +845,7 @@ 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 + return ALL_INDEX end -- Get latest credential table @@ -676,7 +857,7 @@ local function delete_credential_from_table(device, credIdx) )) -- Delete an entry from credential table - local userIdx = 0 + local userIdx = nil for index, entry in pairs(cred_table) do if entry.credentialIndex == credIdx then table.remove(cred_table, index) @@ -717,16 +898,6 @@ end ----------------------------- -- Week Day Schedule Table -- ----------------------------- -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - 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( @@ -980,6 +1151,76 @@ local function delete_year_schedule_from_table_as_user(device, userIdx) 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 -- -------------- @@ -990,32 +1231,26 @@ local function handle_add_user(driver, device, command) local userType = command.args.userType -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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_COTA_INDEX, {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_COTA_INDEX)) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) end ----------------- @@ -1026,33 +1261,28 @@ local function handle_update_user(driver, device, command) local cmdName = "updateUser" local userIdx = command.args.userIndex local userName = command.args.userName - local userType = command.args.lockUserType + 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 - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1086,19 +1316,14 @@ local function get_user_response_handler(driver, device, ib, response) end if status ~= "success" then -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1137,19 +1362,14 @@ local function get_user_response_handler(driver, device, ib, response) ) elseif userIdx >= maxUser then -- There's no available user index -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = "resourceExhausted" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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)) @@ -1163,6 +1383,7 @@ 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 @@ -1176,28 +1397,23 @@ local function set_user_response_handler(driver, device, ib, response) -- Update User in table if status == "success" then if cmdName == "addUser" then - add_user_to_table(device, userIdx, userType) + add_user_to_table(device, userIdx, userName, userType) elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userType) + 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 result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1210,20 +1426,14 @@ local function handle_delete_user(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1244,20 +1454,14 @@ local function handle_delete_all_users(driver, device, command) local cmdName = "deleteAllUsers" -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1288,6 +1492,7 @@ local function clear_user_response_handler(driver, device, ib, response) 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 @@ -1295,18 +1500,14 @@ local function clear_user_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - }) - device:emit_event(event) + 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 @@ -1327,25 +1528,19 @@ local function handle_add_credential(driver, device, command) end local credential = { credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_COTA_INDEX + credential_index = INITIAL_CREDENTIAL_INDEX } local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1353,7 +1548,7 @@ local function handle_add_credential(driver, device, command) 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_COTA_INDEX, {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 @@ -1386,20 +1581,14 @@ local function handle_update_credential(driver, device, command) local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1423,19 +1612,10 @@ local function handle_update_credential(driver, device, command) ) end ------------------------------ --- Set Credential Response -- ------------------------------ -local RESPONSE_STATUS_MAP = { - [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 function set_credential_response_handler(driver, device, ib, response) +--------------------------------- +-- 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 @@ -1449,9 +1629,10 @@ local function set_credential_response_handler(driver, device, ib, response) 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 status = "success" local elements = ib.info_block.data.elements - if elements.status.value == DoorLock.types.DlStatus.SUCCESS then + 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}) @@ -1460,7 +1641,7 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - add_user_to_table(device, elements.user_index.value, userType) + add_user_to_table(device, elements.user_index.value, nil, userType) end -- Update Credential table @@ -1470,20 +1651,15 @@ local function set_credential_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, credentialIndex = credIdx, statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) -- 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}) @@ -1513,33 +1689,20 @@ local function set_credential_response_handler(driver, device, ib, response) return end - -- Update commandResult - status = RESPONSE_STATUS_MAP[elements.status.value] + -- 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)) - - -- Set commandResult to error status - if 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) - return - elseif status ~= "occupied" then - local result = { + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { commandName = cmdName, - statusCode = status + statusCode = "resourceExhausted" -- No more available credential index } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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}) - return - end - - if elements.next_credential_index.value ~= nil then + elseif status == "occupied" then -- Get parameters local credIdx = elements.next_credential_index.value local credential = { @@ -1572,60 +1735,264 @@ local function set_credential_response_handler(driver, device, ib, response) 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 result = { + local command_result_info = { commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 ------------------------ --- 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, - } +----------------------------------- +-- 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] - -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + 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, - statusCode = "busy" + userIndex = userIdx, + requestId = reqId, + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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.CRED_INDEX, credIdx, {persist = true}) + -- 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 + } - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + -- 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 + 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 + 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 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 ---------------------------- @@ -1640,20 +2007,14 @@ local function handle_delete_all_credentials(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1670,42 +2031,71 @@ end -- Clear Credential Response -- ------------------------------- local function clear_credential_response_handler(driver, device, ib, response) - -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credIdx = device:get_field(lock_utils.CRED_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" + 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 - -- Delete User in table - local userIdx = 0 - if status == "success" then + 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 == 0 then - userIdx = nil + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) end - else - device.log.warn(string.format("Failed to clear credential: %s", status)) + + -- 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 - local result = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1730,20 +2120,14 @@ local function handle_set_week_day_schedule(driver, device, command) local endMinute = schedule.endMinute -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1808,20 +2192,15 @@ local function set_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1835,20 +2214,14 @@ local function handle_clear_week_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1885,20 +2258,15 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1942,20 +2310,14 @@ local function handle_set_year_day_schedule(driver, device, command) local localEndTime = command.args.schedule.localEndTime -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2011,20 +2373,15 @@ local function set_year_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -2038,20 +2395,14 @@ local function handle_clear_year_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2160,6 +2511,354 @@ local function handle_refresh(driver, device, command) 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 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, 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, + hex_string_to_octet_string(signingKey), + hex_string_to_octet_string(verificationKey), + hex_string_to_octet_string(groupId), -- Group identification + hex_string_to_octet_string(groupResolvingKey) -- Group resolving key + ) + ) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local verificationKey = device:get_field(lock_utils.VERIFICATION_KEY) + local groupId = device:get_field(lock_utils.GROUP_ID) + local groupResolvingKey = device:get_field(lock_utils.GROUP_RESOLVING_KEY) + + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + elseif ib.status == DoorLock.types.DlStatus.SUCCESS then + if verificationKey ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + verificationKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupId ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + groupId, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupResolvingKey ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + groupResolvingKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + 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 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 + 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 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 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 + 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 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 = { @@ -2181,6 +2880,14 @@ local new_matter_lock_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, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, @@ -2204,6 +2911,7 @@ local new_matter_lock_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, }, }, }, @@ -2233,6 +2941,14 @@ local new_matter_lock_handler = { [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 = { 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 6067d056ff..ab9ac68c88 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 @@ -971,7 +971,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users({{userIndex = 1, userType = "adminMember"}}, {visibility={displayed=false}}) + capabilities.lockUsers.users({{userIndex = 1, userName="Guest1", userType = "adminMember"}}, {visibility={displayed=false}}) ) ) test.socket.capability:__expect_send( @@ -1493,7 +1493,6 @@ test.register_coroutine_test( ), } ) - -- test.wait_for_events() end ) @@ -1745,11 +1744,29 @@ test.register_coroutine_test( 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.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - {commandName="deleteAllCredentials", credentialIndex=65534, statusCode="success"}, + {commandName="deleteAllCredentials", userIndex=65534, credentialIndex=65534, statusCode="success"}, {state_change=true, visibility={displayed=false}} ) ) diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml index 15c766fbdb..c15783f6d2 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -5,6 +5,8 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: serviceArea version: 1 - id: refresh @@ -13,119 +15,6 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml index a83c7801a0..33af113030 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml @@ -5,122 +5,14 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: firmwareUpdate version: 1 - id: refresh version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml index 8f5ca7bc21..560eda12fa 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml @@ -13,75 +13,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc.yml b/drivers/SmartThings/matter-rvc/profiles/rvc.yml index 7246590f4e..84bed73495 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc.yml @@ -11,72 +11,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 4165535fef..25a202241a 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -32,10 +32,11 @@ if version.api < 13 then clusters.Global = require "Global" end -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" +local CURRENT_RUN_MODE = "__current_run_mode" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" local OPERATING_STATE_SUPPORTED_COMMANDS = "__operating_state_supported_commands" +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local subscribed_attributes = { [capabilities.mode.ID] = { @@ -54,32 +55,19 @@ local subscribed_attributes = { } } -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - else - return device.MATTER_DEFAULT_ENDPOINT +local function find_default_endpoint(device, cluster_id) + local eps = device:get_endpoints(cluster_id) + 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 device.MATTER_DEFAULT_ENDPOINT end -local function device_added(driver, device) - local run_mode_eps = device:get_endpoints(clusters.RvcRunMode.ID) or {} - local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} - local component_to_endpoint_map = { - ["main"] = run_mode_eps[1], - ["runMode"] = run_mode_eps[1], - ["cleanMode"] = clean_mode_eps[1] - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) -end - -local function device_init(driver, device) - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) -end - -local function do_configure(driver, device) +local function match_profile(driver, device) local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} local service_area_eps = embedded_cluster_utils.get_endpoints(device, clusters.ServiceArea.ID) or {} @@ -93,6 +81,25 @@ local function do_configure(driver, device) device.log.info_with({hub_logs = true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) +end + +local function device_init(driver, device) + device:subscribe() + + -- comp/ep map functionality removed 9/5/25. + device:set_field("__component_to_endpoint_map", nil) + + if not device:get_field(SERVICE_AREA_PROFILED) then + if #device:get_endpoints(clusters.ServiceArea.ID) > 0 then + match_profile(driver, device) + end + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) end @@ -159,7 +166,7 @@ local function can_send_state_command(device, command_name, current_state, curre return false end -local function update_supported_arguments(device, current_run_mode, current_state) +local function update_supported_arguments(device, ep, current_run_mode, current_state) device.log.info(string.format("update_supported_arguments: %s, %s", current_run_mode, current_state)) if current_run_mode == nil or current_state == nil then return @@ -170,15 +177,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( {}, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - -- Set runMode to empty - event = capabilities.mode.supportedArguments({}, {visibility = {displayed = false}}) - device:emit_component_event(device.profile.components["runMode"], event) - -- Set cleanMode to empty - local component = device.profile.components["cleanMode"] - if component ~= nil then - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) return end @@ -198,7 +197,6 @@ local function update_supported_arguments(device, current_run_mode, current_stat -- Set Supported Operating State Commands local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState local supported_op_commands = {} if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, nil) == true then @@ -213,42 +211,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - - -- Check whether non-idle mode can be selected or not - local can_be_non_idle = false - if current_tag == clusters.RvcRunMode.types.ModeTag.IDLE and - (current_state == cap_op_enum.stopped.NAME or current_state == cap_op_enum.paused.NAME or - current_state == cap_op_enum.docked.NAME or current_state == cap_op_enum.charging.NAME) then - can_be_non_idle = true - end - - -- Set supported run arguments - local supported_arguments = {} -- For generic plugin - for _, mode in ipairs(supported_run_modes) do - if mode.tag == clusters.RvcRunMode.types.ModeTag.IDLE or can_be_non_idle == true then - table.insert(supported_arguments, mode.label) - end - end - - -- Send event to set supported run arguments - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - - -- Set supported clean arguments - local supported_clean_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - supported_arguments = {} - for _, mode in ipairs(supported_clean_modes) do - table.insert(supported_arguments, mode.label) - end - - -- Send event to set supported clean modes - local component = device.profile.components["cleanMode"] - if component ~= nil then - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) end -- Matter Handlers -- @@ -279,23 +242,14 @@ local function run_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(RUN_MODE_SUPPORTED_MODES, supported_modes_id_tag, { persist = true }) - -- Update Supported Modes - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - -- Update Supported Arguments - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_state = device:get_latest_state( "main", capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function run_mode_current_mode_handler(driver, device, ib, response) @@ -315,8 +269,7 @@ local function run_mode_current_mode_handler(driver, device, ib, response) end -- Set current mode - local component = device.profile.components["runMode"] - device:emit_component_event(component, capabilities.mode.mode(current_run_mode)) + device:set_field(CURRENT_RUN_MODE, current_run_mode, { persist = true }) -- Update supported mode local current_state = device:get_latest_state( @@ -324,11 +277,10 @@ local function run_mode_current_mode_handler(driver, device, ib, response) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function clean_mode_supported_mode_handler(driver, device, ib, response) - device.log.info("clean_mode_supported_mode_handler") local supported_modes = {} local supported_modes_id = {} for _, mode in ipairs(ib.data.elements) do @@ -340,11 +292,10 @@ local function clean_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(CLEAN_MODE_SUPPORTED_MODES, supported_modes_id, { persist = true }) - local component = device.profile.components["cleanMode"] local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function clean_mode_current_mode_handler(driver, device, ib, response) @@ -353,8 +304,7 @@ local function clean_mode_current_mode_handler(driver, device, ib, response) local supported_clean_mode = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_clean_mode) do if mode.id == mode_id then - local component = device.profile.components["cleanMode"] - device:emit_component_event(component, capabilities.mode.mode(mode.label)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode.label)) break end end @@ -378,15 +328,11 @@ local function rvc_operational_state_attr_handler(driver, device, ib, response) end -- Supported Mode update - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) if ib.data.value ~= clus_op_enum.ERROR then - update_supported_arguments(device, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) else - update_supported_arguments(device, current_run_mode, "Error") + update_supported_arguments(device, ib.endpoint_id, current_run_mode, "Error") end end @@ -438,11 +384,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device device:set_field(OPERATING_STATE_SUPPORTED_COMMANDS, supportedOperatingStateCommands, { persist = true }) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -478,7 +420,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function upper_to_camelcase(name) @@ -523,9 +465,8 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp end -- Update Supported Areas - local component = device.profile.components["main"] local event = capabilities.serviceArea.supportedAreas(supported_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end -- In case selected area is not in supportedarea then should i add to supported area or remove from selectedarea @@ -535,9 +476,19 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo table.insert(selected_areas, areaId.value) end - local component = device.profile.components["main"] + if next(selected_areas) == nil then + local supported_areas = device:get_latest_state( + "main", + capabilities.serviceArea.ID, + capabilities.serviceArea.supportedAreas.NAME + ) + for i, area in ipairs(supported_areas) do + table.insert(selected_areas, area.areaId) + end + end + local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function robot_cleaner_areas_selection_response_handler(driver, device, ib, response) @@ -552,23 +503,18 @@ local function robot_cleaner_areas_selection_response_handler(driver, device, ib else device.log.error(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) local selectedAreas = device:get_latest_state("main", capabilities.serviceArea.ID, capabilities.serviceArea.selectedAreas.NAME) - local component = device.profile.components["main"] local event = capabilities.serviceArea.selectedAreas(selectedAreas, {state_change = true}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end end -- Capability Handlers -- local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_start") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -594,7 +540,7 @@ local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device:send(clusters.RvcOperationalState.commands.Resume(device, endpoint_id)) elseif can_send_state_command(device, capabilities.mode.commands.setMode.NAME, current_state, current_tag) == true then for _, mode in ipairs(supported_run_modes) do - endpoint_id = device:component_to_endpoint("runMode") + endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if mode.tag == clusters.RvcRunMode.types.ModeTag.CLEANING then device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) return @@ -605,37 +551,26 @@ end local function handle_robot_cleaner_operating_state_pause(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_pause") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.Pause(device, endpoint_id)) end local function handle_robot_cleaner_operating_state_go_home(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_go_home") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.GoHome(device, endpoint_id)) end local function handle_robot_cleaner_mode(driver, device, cmd) device.log.info(string.format("handle_robot_cleaner_mode component: %s, mode: %s", cmd.component, cmd.args.mode)) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "runMode" then - local supported_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end - end - elseif cmd.component == "cleanMode" then - local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) + local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} + for _, mode in ipairs(supported_modes) do + if cmd.args.mode == mode.label then + device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) + device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) + return end end end @@ -648,7 +583,7 @@ local function handle_robot_cleaner_areas_selection(driver, device, cmd) for i, areaId in ipairs(cmd.args.areas) do table.insert(selectAreas, uint32_dt(areaId)) end - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if cmd.component == "main" then device:send(clusters.ServiceArea.commands.SelectAreas(device, endpoint_id, selectAreas)) end @@ -657,7 +592,6 @@ end local matter_rvc_driver = { lifecycle_handlers = { init = device_init, - added = device_added, doConfigure = do_configure, infoChanged = info_changed, }, 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 5bed191802..c051f188fb 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -31,6 +31,7 @@ if version.api < 13 then end local APPLICATION_ENDPOINT = 10 +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rvc-clean-mode-service-area.yml"), @@ -64,6 +65,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + mock_device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) test.disable_startup_messages() test.mock_device.add_test_device(mock_device) local subscribed_attributes = { @@ -119,8 +121,6 @@ local RUN_MODES = { CLEANING_MODE, } -local RUN_MODE_LABELS = { RUN_MODES[1].label, RUN_MODES[2].label, RUN_MODES[3].label } - local CLEAN_MODE_1 = { label = "Clean Mode 1", mode = 0, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 1 }) } } local CLEAN_MODE_2 = { label = "Clean Mode 2", mode = 1, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 2 }) } } @@ -143,12 +143,6 @@ local function supported_run_mode_init() } ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedModes(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end local function supported_clean_mode_init() @@ -163,13 +157,13 @@ local function supported_clean_mode_init() }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedModes(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) @@ -223,7 +217,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On changing the run mode to a mode with an IDLE tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an IDLE tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -235,12 +229,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -253,23 +241,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an CLEANING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an CLEANING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -281,12 +257,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -296,23 +266,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an MAPPING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an MAPPING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -324,12 +282,6 @@ test.register_coroutine_test( MAPPING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = MAPPING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -339,18 +291,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -370,7 +310,7 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.mode({value = cleanMode.label}) ) ) @@ -378,24 +318,6 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "On changing the rvc run mode, appropriate RvcRunMode command must be sent to the device", function() - supported_run_mode_init() - operating_state_init() - test.wait_for_events() - for _, runMode in ipairs(RUN_MODES) do - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "mode", component = "runMode", command = "setMode", args = { runMode.label } } - }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.RvcRunMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, runMode.mode) - }) - end - end -) - test.register_coroutine_test( "On changing the rvc clean mode, appropriate RvcCleanMode command must be sent to the device", function() supported_clean_mode_init() @@ -404,7 +326,7 @@ test.register_coroutine_test( for _, cleanMode in ipairs(CLEAN_MODES) do test.socket.capability:__queue_receive({ mock_device.id, - { capability = "mode", component = "cleanMode", command = "setMode", args = { cleanMode.label } } + { capability = "mode", component = "main", command = "setMode", args = { cleanMode.label } } }) test.socket.matter:__expect_send({ mock_device.id, @@ -415,7 +337,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the Start Command, supportedArgument must be set to the appropriate value", function() + "On receive the start Command of the capability, ChangeToMode command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -428,12 +350,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -446,18 +362,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -471,7 +375,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the goHome Command, supportedArgument must be set to the appropriate value", function() + "On receive the goHome Command of the capability, GoHome command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -484,12 +388,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -499,18 +397,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -524,7 +410,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the pause Command, supportedArgument must be set to the appropriate value", function() + "On receive the pause Command of the capability, Pause command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -537,12 +423,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -552,18 +432,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -590,18 +458,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -627,12 +483,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -645,18 +495,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -680,18 +518,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -708,12 +534,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -726,18 +546,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -764,18 +572,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -792,12 +588,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -810,18 +600,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -848,18 +626,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -876,12 +642,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -894,18 +654,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -929,18 +677,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -957,12 +693,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -975,18 +705,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1010,18 +728,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -1038,12 +744,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1056,18 +756,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1085,18 +773,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalError:build_test_report_data( From 0aa75db6f144a42ae35cda64303e79d637167090 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 17 Sep 2025 13:38:02 -0500 Subject: [PATCH 131/449] Prod hotfix: matter rvc UX updates and matter lock subdriver Aliro support (#2406) * Updating matter rvc ux (#2343) Signed-off-by: HunsupJung * Update matter-rvc driver (#2400) - Replace Embedded device configuration with Device presentation to support translation - If selected_area is empty, selecting all areas Signed-off-by: HunsupJung * Update new-matter-lock driver to support Aliro feature (#2344) Signed-off-by: Hunsup Jung Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --------- Signed-off-by: HunsupJung Signed-off-by: Hunsup Jung Co-authored-by: HunsupJung <59987061+HunsupJung@users.noreply.github.com> --- .../lock-modular-embedded-unlatch.yml | 3 + .../matter-lock/profiles/lock-modular.yml | 3 + .../matter-lock/src/lock_utils.lua | 12 +- .../matter-lock/src/new-matter-lock/init.lua | 1368 +++++++++++++---- .../src/test/test_new_matter_lock.lua | 23 +- .../profiles/rvc-clean-mode-service-area.yml | 121 +- .../matter-rvc/profiles/rvc-clean-mode.yml | 118 +- .../matter-rvc/profiles/rvc-service-area.yml | 72 - .../SmartThings/matter-rvc/profiles/rvc.yml | 69 - drivers/SmartThings/matter-rvc/src/init.lua | 212 +-- .../matter-rvc/src/test/test_matter_rvc.lua | 348 +---- 11 files changed, 1174 insertions(+), 1175 deletions(-) diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml index 4ea6ba1e0d..3d9e68b44c 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular-embedded-unlatch.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml index 576695f873..3a8a53bf70 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-modular.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-modular.yml @@ -17,6 +17,9 @@ components: - id: lockSchedules version: 1 optional: true + - id: lockAliro + version: 1 + optional: true - id: battery version: 1 optional: true diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 5d92c55afa..816ca446f2 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -40,7 +40,17 @@ local lock_utils = { SCHEDULE_END_HOUR = "scheduleEndHour", SCHEDULE_END_MINUTE = "scheduleEndMinute", SCHEDULE_LOCAL_START_TIME = "scheduleLocalStartTime", - SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime" + SCHEDULE_LOCAL_END_TIME = "scheduleLocalEndTime", + VERIFICATION_KEY = "verificationKey", + GROUP_ID = "groupId", + GROUP_RESOLVING_KEY = "groupResolvingKey", + ISSUER_KEY = "issuerKey", + ISSUER_KEY_INDEX = "issuerKeyIndex", + ENDPOINT_KEY = "endpointKey", + ENDPOINT_KEY_INDEX = "endpointKeyIndex", + ENDPOINT_KEY_TYPE = "endpointKeyType", + DEVICE_KEY_ID = "deviceKeyId", + COMMAND_REQUEST_ID = "commandRequestId" } local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index aad7046a3d..f9c06f193c 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -27,12 +27,37 @@ end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local INITIAL_COTA_INDEX = 1 +local INITIAL_CREDENTIAL_INDEX = 1 local ALL_INDEX = 0xFFFE 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 NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 @@ -86,6 +111,17 @@ local subscribed_attributes = { 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 }, @@ -193,6 +229,9 @@ local function match_profile_modular(driver, device) 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 @@ -295,7 +334,7 @@ local function driver_switched(driver, device) end -- This function check busy_state and if busy_state is false, set it to true(current time) -local function check_busy_state(device) +local function is_busy_state_set(device) local c_time = os.time() local busy_state = device:get_field(lock_utils.BUSY_STATE) or false if busy_state == false or c_time - busy_state > 10 then @@ -404,7 +443,7 @@ local function set_cota_credential(device, credential_index) end -- Check Busy State - if check_busy_state(device) == true then + if 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) @@ -451,7 +490,7 @@ local function apply_cota_credentials_if_absent(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) + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) end) end) end @@ -479,6 +518,113 @@ 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 Util -- +---------------- +local function hex_string_to_octet_string(hex_string) + if hex_string == nil then + return nil + end + local octet_string = "" + for i = 1, #hex_string, 2 do + local hex = hex_string:sub(i, i + 1) + octet_string = octet_string .. string.char(tonumber(hex, 16)) + end + return octet_string +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}} + )) + 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 + --------------------------------- -- Power Source Attribute List -- --------------------------------- @@ -579,7 +725,7 @@ end ---------------- -- User Table -- ---------------- -local function add_user_to_table(device, userIdx, usrType) +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", @@ -589,11 +735,11 @@ local function add_user_to_table(device, userIdx, usrType) )) -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userType = usrType}) + 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, usrType) +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", @@ -613,7 +759,8 @@ local function update_user_in_table(device, userIdx, usrType) -- Update user entry if i ~= 0 then - user_table[i].userType = usrType + user_table[i].userType = userType + user_table[i].userName = userName device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) end end @@ -646,6 +793,40 @@ 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( @@ -664,7 +845,7 @@ 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 + return ALL_INDEX end -- Get latest credential table @@ -676,7 +857,7 @@ local function delete_credential_from_table(device, credIdx) )) -- Delete an entry from credential table - local userIdx = 0 + local userIdx = nil for index, entry in pairs(cred_table) do if entry.credentialIndex == credIdx then table.remove(cred_table, index) @@ -717,16 +898,6 @@ end ----------------------------- -- Week Day Schedule Table -- ----------------------------- -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - 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( @@ -980,6 +1151,76 @@ local function delete_year_schedule_from_table_as_user(device, userIdx) 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 -- -------------- @@ -990,32 +1231,26 @@ local function handle_add_user(driver, device, command) local userType = command.args.userType -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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_COTA_INDEX, {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_COTA_INDEX)) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) end ----------------- @@ -1026,33 +1261,28 @@ local function handle_update_user(driver, device, command) local cmdName = "updateUser" local userIdx = command.args.userIndex local userName = command.args.userName - local userType = command.args.lockUserType + 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 - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1086,19 +1316,14 @@ local function get_user_response_handler(driver, device, ib, response) end if status ~= "success" then -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1137,19 +1362,14 @@ local function get_user_response_handler(driver, device, ib, response) ) elseif userIdx >= maxUser then -- There's no available user index -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = "resourceExhausted" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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)) @@ -1163,6 +1383,7 @@ 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 @@ -1176,28 +1397,23 @@ local function set_user_response_handler(driver, device, ib, response) -- Update User in table if status == "success" then if cmdName == "addUser" then - add_user_to_table(device, userIdx, userType) + add_user_to_table(device, userIdx, userName, userType) elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userType) + 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 result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1210,20 +1426,14 @@ local function handle_delete_user(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1244,20 +1454,14 @@ local function handle_delete_all_users(driver, device, command) local cmdName = "deleteAllUsers" -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1288,6 +1492,7 @@ local function clear_user_response_handler(driver, device, ib, response) 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 @@ -1295,18 +1500,14 @@ local function clear_user_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, statusCode = status } - local event = capabilities.lockUsers.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - }) - device:emit_event(event) + 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 @@ -1327,25 +1528,19 @@ local function handle_add_credential(driver, device, command) end local credential = { credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_COTA_INDEX + credential_index = INITIAL_CREDENTIAL_INDEX } local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1353,7 +1548,7 @@ local function handle_add_credential(driver, device, command) 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_COTA_INDEX, {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 @@ -1386,20 +1581,14 @@ local function handle_update_credential(driver, device, command) local credData = command.args.credentialData -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1423,19 +1612,10 @@ local function handle_update_credential(driver, device, command) ) end ------------------------------ --- Set Credential Response -- ------------------------------ -local RESPONSE_STATUS_MAP = { - [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 function set_credential_response_handler(driver, device, ib, response) +--------------------------------- +-- 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 @@ -1449,9 +1629,10 @@ local function set_credential_response_handler(driver, device, ib, response) 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 status = "success" local elements = ib.info_block.data.elements - if elements.status.value == DoorLock.types.DlStatus.SUCCESS then + 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}) @@ -1460,7 +1641,7 @@ local function set_credential_response_handler(driver, device, ib, response) -- If user is added also, update User table if userIdx == nil then - add_user_to_table(device, elements.user_index.value, userType) + add_user_to_table(device, elements.user_index.value, nil, userType) end -- Update Credential table @@ -1470,20 +1651,15 @@ local function set_credential_response_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, credentialIndex = credIdx, statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) -- 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}) @@ -1513,33 +1689,20 @@ local function set_credential_response_handler(driver, device, ib, response) return end - -- Update commandResult - status = RESPONSE_STATUS_MAP[elements.status.value] + -- 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)) - - -- Set commandResult to error status - if 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) - return - elseif status ~= "occupied" then - local result = { + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { commandName = cmdName, - statusCode = status + statusCode = "resourceExhausted" -- No more available credential index } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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}) - return - end - - if elements.next_credential_index.value ~= nil then + elseif status == "occupied" then -- Get parameters local credIdx = elements.next_credential_index.value local credential = { @@ -1572,60 +1735,264 @@ local function set_credential_response_handler(driver, device, ib, response) 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 result = { + local command_result_info = { commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 ------------------------ --- 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, - } +----------------------------------- +-- 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] - -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + 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, - statusCode = "busy" + userIndex = userIdx, + requestId = reqId, + statusCode = status } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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.CRED_INDEX, credIdx, {persist = true}) + -- 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 + } - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + -- 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 + 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 + 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 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 ---------------------------- @@ -1640,20 +2007,14 @@ local function handle_delete_all_credentials(driver, device, command) } -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1670,42 +2031,71 @@ end -- Clear Credential Response -- ------------------------------- local function clear_credential_response_handler(driver, device, ib, response) - -- Get result local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credIdx = device:get_field(lock_utils.CRED_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" + 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 - -- Delete User in table - local userIdx = 0 - if status == "success" then + 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 == 0 then - userIdx = nil + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) end - else - device.log.warn(string.format("Failed to clear credential: %s", status)) + + -- 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 - local result = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - local event = capabilities.lockCredentials.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1730,20 +2120,14 @@ local function handle_set_week_day_schedule(driver, device, command) local endMinute = schedule.endMinute -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1808,20 +2192,15 @@ local function set_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1835,20 +2214,14 @@ local function handle_clear_week_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -1885,20 +2258,15 @@ local function clear_week_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -1942,20 +2310,14 @@ local function handle_set_year_day_schedule(driver, device, command) local localEndTime = command.args.schedule.localEndTime -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2011,20 +2373,15 @@ local function set_year_day_schedule_handler(driver, device, ib, response) end -- Update commandResult - local result = { + local command_result_info = { commandName = cmdName, userIndex = userIdx, scheduleIndex = scheduleIdx, statusCode = status } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + 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 @@ -2038,20 +2395,14 @@ local function handle_clear_year_day_schedule(driver, device, command) local userIdx = command.args.userIndex -- Check busy state - local busy = check_busy_state(device) - if busy == true then - local result = { + if is_busy_state_set(device) then + local command_result_info = { commandName = cmdName, statusCode = "busy" } - local event = capabilities.lockSchedules.commandResult( - result, - { - state_change = true, - visibility = {displayed = false} - } - ) - device:emit_event(event) + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end @@ -2160,6 +2511,354 @@ local function handle_refresh(driver, device, command) 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 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, 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, + hex_string_to_octet_string(signingKey), + hex_string_to_octet_string(verificationKey), + hex_string_to_octet_string(groupId), -- Group identification + hex_string_to_octet_string(groupResolvingKey) -- Group resolving key + ) + ) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local verificationKey = device:get_field(lock_utils.VERIFICATION_KEY) + local groupId = device:get_field(lock_utils.GROUP_ID) + local groupResolvingKey = device:get_field(lock_utils.GROUP_RESOLVING_KEY) + + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + elseif ib.status == DoorLock.types.DlStatus.SUCCESS then + if verificationKey ~= nil then + device:emit_event(capabilities.lockAliro.readerVerificationKey( + verificationKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupId ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + groupId, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + if groupResolvingKey ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + groupResolvingKey, + { + state_change = true, + visibility = {displayed = false} + } + )) + end + 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 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 + 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 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 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 + 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 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 = { @@ -2181,6 +2880,14 @@ local new_matter_lock_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, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, @@ -2204,6 +2911,7 @@ local new_matter_lock_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, }, }, }, @@ -2233,6 +2941,14 @@ local new_matter_lock_handler = { [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 = { 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 6067d056ff..ab9ac68c88 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 @@ -971,7 +971,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockUsers.users({{userIndex = 1, userType = "adminMember"}}, {visibility={displayed=false}}) + capabilities.lockUsers.users({{userIndex = 1, userName="Guest1", userType = "adminMember"}}, {visibility={displayed=false}}) ) ) test.socket.capability:__expect_send( @@ -1493,7 +1493,6 @@ test.register_coroutine_test( ), } ) - -- test.wait_for_events() end ) @@ -1745,11 +1744,29 @@ test.register_coroutine_test( 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.lockSchedules.weekDaySchedules({}, {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules({}, {visibility={displayed=false}}) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.lockCredentials.commandResult( - {commandName="deleteAllCredentials", credentialIndex=65534, statusCode="success"}, + {commandName="deleteAllCredentials", userIndex=65534, credentialIndex=65534, statusCode="success"}, {state_change=true, visibility={displayed=false}} ) ) diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml index 15c766fbdb..c15783f6d2 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode-service-area.yml @@ -5,6 +5,8 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: serviceArea version: 1 - id: refresh @@ -13,119 +15,6 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml index a83c7801a0..33af113030 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-clean-mode.yml @@ -5,122 +5,14 @@ components: capabilities: - id: robotCleanerOperatingState version: 1 + - id: mode + version: 1 - id: firmwareUpdate version: 1 - id: refresh version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner - - id: cleanMode - label: Clean mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - - component: cleanMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list +metadata: + mnmn: SmartThingsEdge + vid: generic-rvc diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml index 8f5ca7bc21..560eda12fa 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc-service-area.yml @@ -13,75 +13,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: main - capability: serviceArea - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list \ No newline at end of file diff --git a/drivers/SmartThings/matter-rvc/profiles/rvc.yml b/drivers/SmartThings/matter-rvc/profiles/rvc.yml index 7246590f4e..84bed73495 100644 --- a/drivers/SmartThings/matter-rvc/profiles/rvc.yml +++ b/drivers/SmartThings/matter-rvc/profiles/rvc.yml @@ -11,72 +11,3 @@ components: version: 1 categories: - name: RobotCleaner - - id: runMode - label: Run mode - capabilities: - - id: mode - version: 1 - categories: - - name: RobotCleaner -deviceConfig: - dashboard: - states: - - component: main - capability: robotCleanerOperatingState - version: 1 - detailView: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/list/command/supportedValues - value: supportedArguments.value - - component: main - capability: refresh - version: 1 - - component: main - capability: firmwareUpdate - version: 1 - automation: - conditions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - value: mode.value - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list - actions: - - component: main - capability: robotCleanerOperatingState - version: 1 - - component: runMode - capability: mode - version: 1 - patch: - - op: replace - path: /0/displayType - value: dynamicList - - op: add - path: /0/dynamicList - value: - command: setMode - supportedValues: - value: supportedModes.value - - op: remove - path: /0/list diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 4165535fef..25a202241a 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -32,10 +32,11 @@ if version.api < 13 then clusters.Global = require "Global" end -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" +local CURRENT_RUN_MODE = "__current_run_mode" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" local OPERATING_STATE_SUPPORTED_COMMANDS = "__operating_state_supported_commands" +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local subscribed_attributes = { [capabilities.mode.ID] = { @@ -54,32 +55,19 @@ local subscribed_attributes = { } } -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - else - return device.MATTER_DEFAULT_ENDPOINT +local function find_default_endpoint(device, cluster_id) + local eps = device:get_endpoints(cluster_id) + 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 device.MATTER_DEFAULT_ENDPOINT end -local function device_added(driver, device) - local run_mode_eps = device:get_endpoints(clusters.RvcRunMode.ID) or {} - local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} - local component_to_endpoint_map = { - ["main"] = run_mode_eps[1], - ["runMode"] = run_mode_eps[1], - ["cleanMode"] = clean_mode_eps[1] - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) -end - -local function device_init(driver, device) - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) -end - -local function do_configure(driver, device) +local function match_profile(driver, device) local clean_mode_eps = device:get_endpoints(clusters.RvcCleanMode.ID) or {} local service_area_eps = embedded_cluster_utils.get_endpoints(device, clusters.ServiceArea.ID) or {} @@ -93,6 +81,25 @@ local function do_configure(driver, device) device.log.info_with({hub_logs = true}, string.format("Updating device profile to %s.", profile_name)) device:try_update_metadata({profile = profile_name}) +end + +local function device_init(driver, device) + device:subscribe() + + -- comp/ep map functionality removed 9/5/25. + device:set_field("__component_to_endpoint_map", nil) + + if not device:get_field(SERVICE_AREA_PROFILED) then + if #device:get_endpoints(clusters.ServiceArea.ID) > 0 then + match_profile(driver, device) + end + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + end +end + +local function do_configure(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) end @@ -159,7 +166,7 @@ local function can_send_state_command(device, command_name, current_state, curre return false end -local function update_supported_arguments(device, current_run_mode, current_state) +local function update_supported_arguments(device, ep, current_run_mode, current_state) device.log.info(string.format("update_supported_arguments: %s, %s", current_run_mode, current_state)) if current_run_mode == nil or current_state == nil then return @@ -170,15 +177,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( {}, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - -- Set runMode to empty - event = capabilities.mode.supportedArguments({}, {visibility = {displayed = false}}) - device:emit_component_event(device.profile.components["runMode"], event) - -- Set cleanMode to empty - local component = device.profile.components["cleanMode"] - if component ~= nil then - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) return end @@ -198,7 +197,6 @@ local function update_supported_arguments(device, current_run_mode, current_stat -- Set Supported Operating State Commands local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState local supported_op_commands = {} if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, nil) == true then @@ -213,42 +211,7 @@ local function update_supported_arguments(device, current_run_mode, current_stat local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) - - -- Check whether non-idle mode can be selected or not - local can_be_non_idle = false - if current_tag == clusters.RvcRunMode.types.ModeTag.IDLE and - (current_state == cap_op_enum.stopped.NAME or current_state == cap_op_enum.paused.NAME or - current_state == cap_op_enum.docked.NAME or current_state == cap_op_enum.charging.NAME) then - can_be_non_idle = true - end - - -- Set supported run arguments - local supported_arguments = {} -- For generic plugin - for _, mode in ipairs(supported_run_modes) do - if mode.tag == clusters.RvcRunMode.types.ModeTag.IDLE or can_be_non_idle == true then - table.insert(supported_arguments, mode.label) - end - end - - -- Send event to set supported run arguments - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - - -- Set supported clean arguments - local supported_clean_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - supported_arguments = {} - for _, mode in ipairs(supported_clean_modes) do - table.insert(supported_arguments, mode.label) - end - - -- Send event to set supported clean modes - local component = device.profile.components["cleanMode"] - if component ~= nil then - local event = capabilities.mode.supportedArguments(supported_arguments, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - end + device:emit_event_for_endpoint(ep, event) end -- Matter Handlers -- @@ -279,23 +242,14 @@ local function run_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(RUN_MODE_SUPPORTED_MODES, supported_modes_id_tag, { persist = true }) - -- Update Supported Modes - local component = device.profile.components["runMode"] - local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) - -- Update Supported Arguments - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_state = device:get_latest_state( "main", capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function run_mode_current_mode_handler(driver, device, ib, response) @@ -315,8 +269,7 @@ local function run_mode_current_mode_handler(driver, device, ib, response) end -- Set current mode - local component = device.profile.components["runMode"] - device:emit_component_event(component, capabilities.mode.mode(current_run_mode)) + device:set_field(CURRENT_RUN_MODE, current_run_mode, { persist = true }) -- Update supported mode local current_state = device:get_latest_state( @@ -324,11 +277,10 @@ local function run_mode_current_mode_handler(driver, device, ib, response) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - update_supported_arguments(device, current_run_mode, current_state) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, current_state) end local function clean_mode_supported_mode_handler(driver, device, ib, response) - device.log.info("clean_mode_supported_mode_handler") local supported_modes = {} local supported_modes_id = {} for _, mode in ipairs(ib.data.elements) do @@ -340,11 +292,10 @@ local function clean_mode_supported_mode_handler(driver, device, ib, response) end device:set_field(CLEAN_MODE_SUPPORTED_MODES, supported_modes_id, { persist = true }) - local component = device.profile.components["cleanMode"] local event = capabilities.mode.supportedModes(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supported_modes, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function clean_mode_current_mode_handler(driver, device, ib, response) @@ -353,8 +304,7 @@ local function clean_mode_current_mode_handler(driver, device, ib, response) local supported_clean_mode = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_clean_mode) do if mode.id == mode_id then - local component = device.profile.components["cleanMode"] - device:emit_component_event(component, capabilities.mode.mode(mode.label)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode.label)) break end end @@ -378,15 +328,11 @@ local function rvc_operational_state_attr_handler(driver, device, ib, response) end -- Supported Mode update - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) if ib.data.value ~= clus_op_enum.ERROR then - update_supported_arguments(device, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) + update_supported_arguments(device, ib.endpoint_id, current_run_mode, OPERATING_STATE_MAP[ib.data.value].NAME) else - update_supported_arguments(device, current_run_mode, "Error") + update_supported_arguments(device, ib.endpoint_id, current_run_mode, "Error") end end @@ -438,11 +384,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device device:set_field(OPERATING_STATE_SUPPORTED_COMMANDS, supportedOperatingStateCommands, { persist = true }) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -478,7 +420,7 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device local event = capabilities.robotCleanerOperatingState.supportedOperatingStateCommands( supported_op_commands, {visibility = {displayed = false}} ) - device:emit_component_event(device.profile.components["main"], event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function upper_to_camelcase(name) @@ -523,9 +465,8 @@ local function rvc_service_area_supported_areas_handler(driver, device, ib, resp end -- Update Supported Areas - local component = device.profile.components["main"] local event = capabilities.serviceArea.supportedAreas(supported_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end -- In case selected area is not in supportedarea then should i add to supported area or remove from selectedarea @@ -535,9 +476,19 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo table.insert(selected_areas, areaId.value) end - local component = device.profile.components["main"] + if next(selected_areas) == nil then + local supported_areas = device:get_latest_state( + "main", + capabilities.serviceArea.ID, + capabilities.serviceArea.supportedAreas.NAME + ) + for i, area in ipairs(supported_areas) do + table.insert(selected_areas, area.areaId) + end + end + local event = capabilities.serviceArea.selectedAreas(selected_areas, {visibility = {displayed = false}}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end local function robot_cleaner_areas_selection_response_handler(driver, device, ib, response) @@ -552,23 +503,18 @@ local function robot_cleaner_areas_selection_response_handler(driver, device, ib else device.log.error(string.format("robot_cleaner_areas_selection_response_handler: %s, %s",status.pretty_print(status),status_text)) local selectedAreas = device:get_latest_state("main", capabilities.serviceArea.ID, capabilities.serviceArea.selectedAreas.NAME) - local component = device.profile.components["main"] local event = capabilities.serviceArea.selectedAreas(selectedAreas, {state_change = true}) - device:emit_component_event(component, event) + device:emit_event_for_endpoint(ib.endpoint_id, event) end end -- Capability Handlers -- local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_start") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) -- Get current run mode, current tag, current operating state - local current_run_mode = device:get_latest_state( - "runMode", - capabilities.mode.ID, - capabilities.mode.mode.NAME - ) + local current_run_mode = device:get_field(CURRENT_RUN_MODE) local current_tag = 0xFFFF local supported_run_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} for _, mode in ipairs(supported_run_modes) do @@ -594,7 +540,7 @@ local function handle_robot_cleaner_operating_state_start(driver, device, cmd) device:send(clusters.RvcOperationalState.commands.Resume(device, endpoint_id)) elseif can_send_state_command(device, capabilities.mode.commands.setMode.NAME, current_state, current_tag) == true then for _, mode in ipairs(supported_run_modes) do - endpoint_id = device:component_to_endpoint("runMode") + endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if mode.tag == clusters.RvcRunMode.types.ModeTag.CLEANING then device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) return @@ -605,37 +551,26 @@ end local function handle_robot_cleaner_operating_state_pause(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_pause") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.Pause(device, endpoint_id)) end local function handle_robot_cleaner_operating_state_go_home(driver, device, cmd) device.log.info("handle_robot_cleaner_operating_state_go_home") - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) device:send(clusters.RvcOperationalState.commands.GoHome(device, endpoint_id)) end local function handle_robot_cleaner_mode(driver, device, cmd) device.log.info(string.format("handle_robot_cleaner_mode component: %s, mode: %s", cmd.component, cmd.args.mode)) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "runMode" then - local supported_modes = device:get_field(RUN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcRunMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end - end - elseif cmd.component == "cleanMode" then - local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} - for _, mode in ipairs(supported_modes) do - if cmd.args.mode == mode.label then - device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) - device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) - return - end + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) + local supported_modes = device:get_field(CLEAN_MODE_SUPPORTED_MODES) or {} + for _, mode in ipairs(supported_modes) do + if cmd.args.mode == mode.label then + device.log.info(string.format("mode.label: %s, mode.id: %s", mode.label, mode.id)) + device:send(clusters.RvcCleanMode.commands.ChangeToMode(device, endpoint_id, mode.id)) + return end end end @@ -648,7 +583,7 @@ local function handle_robot_cleaner_areas_selection(driver, device, cmd) for i, areaId in ipairs(cmd.args.areas) do table.insert(selectAreas, uint32_dt(areaId)) end - local endpoint_id = device:component_to_endpoint(cmd.component) + local endpoint_id = find_default_endpoint(device, clusters.RvcOperationalState.ID) if cmd.component == "main" then device:send(clusters.ServiceArea.commands.SelectAreas(device, endpoint_id, selectAreas)) end @@ -657,7 +592,6 @@ end local matter_rvc_driver = { lifecycle_handlers = { init = device_init, - added = device_added, doConfigure = do_configure, infoChanged = info_changed, }, 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 5bed191802..c051f188fb 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -31,6 +31,7 @@ if version.api < 13 then end local APPLICATION_ENDPOINT = 10 +local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rvc-clean-mode-service-area.yml"), @@ -64,6 +65,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local function test_init() + mock_device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) test.disable_startup_messages() test.mock_device.add_test_device(mock_device) local subscribed_attributes = { @@ -119,8 +121,6 @@ local RUN_MODES = { CLEANING_MODE, } -local RUN_MODE_LABELS = { RUN_MODES[1].label, RUN_MODES[2].label, RUN_MODES[3].label } - local CLEAN_MODE_1 = { label = "Clean Mode 1", mode = 0, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 1 }) } } local CLEAN_MODE_2 = { label = "Clean Mode 2", mode = 1, mode_tags = { modeTagStruct({ mfg_code = 0x1E1E, value = 2 }) } } @@ -143,12 +143,6 @@ local function supported_run_mode_init() } ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedModes(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end local function supported_clean_mode_init() @@ -163,13 +157,13 @@ local function supported_clean_mode_init() }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedModes(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) ) ) @@ -223,7 +217,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On changing the run mode to a mode with an IDLE tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an IDLE tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -235,12 +229,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -253,23 +241,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an CLEANING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an CLEANING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -281,12 +257,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -296,23 +266,11 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) test.register_coroutine_test( - "On changing the run mode to a mode with an MAPPING tag, supportedArgument must be set to the appropriate value", function() + "On changing the run mode to a mode with an MAPPING tag, supportedOperatingStateCommands must be set to the appropriate value", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -324,12 +282,6 @@ test.register_coroutine_test( MAPPING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = MAPPING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -339,18 +291,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -370,7 +310,7 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send( mock_device:generate_test_message( - "cleanMode", + "main", capabilities.mode.mode({value = cleanMode.label}) ) ) @@ -378,24 +318,6 @@ test.register_coroutine_test( end ) -test.register_coroutine_test( - "On changing the rvc run mode, appropriate RvcRunMode command must be sent to the device", function() - supported_run_mode_init() - operating_state_init() - test.wait_for_events() - for _, runMode in ipairs(RUN_MODES) do - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = "mode", component = "runMode", command = "setMode", args = { runMode.label } } - }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.RvcRunMode.server.commands.ChangeToMode(mock_device, APPLICATION_ENDPOINT, runMode.mode) - }) - end - end -) - test.register_coroutine_test( "On changing the rvc clean mode, appropriate RvcCleanMode command must be sent to the device", function() supported_clean_mode_init() @@ -404,7 +326,7 @@ test.register_coroutine_test( for _, cleanMode in ipairs(CLEAN_MODES) do test.socket.capability:__queue_receive({ mock_device.id, - { capability = "mode", component = "cleanMode", command = "setMode", args = { cleanMode.label } } + { capability = "mode", component = "main", command = "setMode", args = { cleanMode.label } } }) test.socket.matter:__expect_send({ mock_device.id, @@ -415,7 +337,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the Start Command, supportedArgument must be set to the appropriate value", function() + "On receive the start Command of the capability, ChangeToMode command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -428,12 +350,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -446,18 +362,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -471,7 +375,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the goHome Command, supportedArgument must be set to the appropriate value", function() + "On receive the goHome Command of the capability, GoHome command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -484,12 +388,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -499,18 +397,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -524,7 +410,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "On receive the pause Command, supportedArgument must be set to the appropriate value", function() + "On receive the pause Command of the capability, Pause command must be sent to the device", function() supported_run_mode_init() supported_clean_mode_init() operating_state_init() @@ -537,12 +423,6 @@ test.register_coroutine_test( CLEANING_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = CLEANING_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -552,18 +432,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({IDLE_MODE.label}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -590,18 +458,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, @@ -627,12 +483,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -645,18 +495,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -680,18 +518,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -708,12 +534,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -726,18 +546,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -764,18 +572,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -792,12 +588,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -810,18 +600,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -848,18 +626,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({ IDLE_MODE.label }, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -876,12 +642,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -894,18 +654,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -929,18 +677,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -957,12 +693,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -975,18 +705,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1010,18 +728,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) end ) @@ -1038,12 +744,6 @@ test.register_coroutine_test( IDLE_MODE.mode ) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.mode({value = IDLE_MODE.label}) - ) - ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", @@ -1056,18 +756,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments(RUN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments(CLEAN_MODE_LABELS, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalState:build_test_report_data( @@ -1085,18 +773,6 @@ test.register_coroutine_test( ) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "runMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "cleanMode", - capabilities.mode.supportedArguments({}, { visibility = { displayed = false } }) - ) - ) test.socket.matter:__queue_receive({ mock_device.id, clusters.RvcOperationalState.server.attributes.OperationalError:build_test_report_data( From 3a2b1a6e69597ebfac539bd30e57fd189559de44 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:31:42 -0500 Subject: [PATCH 132/449] remove unused refresh handler (#2397) --- drivers/SmartThings/matter-window-covering/src/init.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index a8e6e80114..b3dd31a05c 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -343,9 +343,6 @@ local matter_driver_template = { }, }, capability_handlers = { - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = nil --TODO: define me! - }, [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_preset, }, From f401740eac2984c0079d9e6a577328ab2944ebc5 Mon Sep 17 00:00:00 2001 From: Seong-Yeol-Park <100197321+Seong-Yeol-Park@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:18:08 +0900 Subject: [PATCH 133/449] Hanssem: Update deviceLabel --- drivers/SmartThings/zigbee-window-treatment/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index 723c0d1064..f77f2db946 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -99,7 +99,7 @@ zigbeeManufacturer: model: Gear deviceProfileName: window-treatment-battery - id: "_TZE204_fzbskaga/TS0601" - deviceLabel: Hanssem Window Treatment + deviceLabel: Window Treatment manufacturer: _TZE204_fzbskaga model: TS0601 deviceProfileName: window-treatment-hanssem From 810a8c40b3cedf620efa416754fcea399022ec3e Mon Sep 17 00:00:00 2001 From: Nick Peterson Date: Thu, 18 Sep 2025 18:24:55 +0000 Subject: [PATCH 134/449] Refactor Jenkins Pipeline (#2393) --- Jenkinsfile | 20 ++++++------------- tools/deploy.py | 51 +++++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 92d915bac5..62bfe12c65 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,13 +28,14 @@ pipeline { agent { docker { image 'python:3.10' - label 'production' + label "${params.NODE_LABEL ?: 'production'}" args '--entrypoint= -u 0:0' } } environment { BRANCH = getEnvName() CHANGED_DRIVERS = getChangedDrivers() + ENVIRONMENT = "${env.NODE_LABEL.toUpperCase()}" } stages { stage('requirements') { @@ -51,22 +52,13 @@ pipeline { } } stage('update') { - matrix { - axes { - axis { - name 'ENVIRONMENT' - values 'DEV', 'STAGING', 'ACCEPTANCE', 'PRODUCTION' - } - } - stages { - stage('environment_update') { - steps { - sh 'python3 tools/deploy.py' - } + stages { + stage('environment_update') { + steps { + sh 'python3 tools/deploy.py' } } } } } } - diff --git a/tools/deploy.py b/tools/deploy.py index c9d92448f5..451229f9a4 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,23 +5,23 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" +DRY_RUN = os.environ.get("DRY_RUN") != None print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) -branch_environment = "{}_{}_".format(BRANCH, ENVIRONMENT) -ENVIRONMENT_URL = os.environ.get(ENVIRONMENT+'_ENVIRONMENT_URL') +ENVIRONMENT_URL = os.environ.get('ENVIRONMENT_URL') if not ENVIRONMENT_URL: print("No environment url specified, aborting.") exit(0) UPLOAD_URL = ENVIRONMENT_URL+"/drivers/package" -CHANNEL_ID = os.environ.get(branch_environment+'CHANNEL_ID') +CHANNEL_ID = os.environ.get(BRANCH+'_CHANNEL_ID') if not CHANNEL_ID: - print("No channel id specified, aborting.") + print("No channel id specified for "+BRANCH+", aborting.") exit(0) UPDATE_URL = ENVIRONMENT_URL+"/channels/"+CHANNEL_ID+"/drivers/bulk" -TOKEN = os.environ.get(ENVIRONMENT+'_TOKEN') +TOKEN = os.environ.get('TOKEN') DRIVERID = "driverId" VERSION = "version" PACKAGEKEY = "packageKey" @@ -91,7 +91,8 @@ headers = { "Accept": "application/vnd.smartthings+json;v=20200810", "Authorization": "Bearer "+TOKEN, - "X-ST-LOG-LEVEL": "DEBUG" + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) }, json = { DRIVERID: driver[DRIVERID], @@ -147,7 +148,7 @@ print(error.stderr) retries += 1 if retries >= 5: - print("5 zip failires, skipping "+package_key+" and continuing.") + print("5 zip failures, skipping "+package_key+" and continuing.") continue with open(driver+".zip", 'rb') as driver_package: data = driver_package.read() @@ -187,23 +188,27 @@ print("Uploading package: {} driver id: {} version: {}".format(package_key, driver_info[DRIVERID], driver_info[VERSION])) driver_updates.append({DRIVERID: driver_info[DRIVERID], VERSION: driver_info[VERSION]}) -response = requests.put( - UPDATE_URL, - headers={ - "Accept": "application/vnd.smartthings+json;v=20200810", - "Authorization": "Bearer "+TOKEN, - "Content-Type": "application/json", - "X-ST-LOG-LEVEL": "DEBUG" - }, - data=json.dumps(driver_updates) -) -if response.status_code != 204: - print("Failed to bulk update drivers") - print("Error code: "+str(response.status_code)) - print("Error response: "+response.text) - exit(1) +if DRY_RUN: + print("Dry Run, skipping bulk upload to " + UPDATE_URL) +else: + response = requests.put( + UPDATE_URL, + headers={ + "Accept": "application/vnd.smartthings+json;v=20200810", + "Authorization": "Bearer "+TOKEN, + "Content-Type": "application/json", + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) + }, + data=json.dumps(driver_updates) + ) + if response.status_code != 204: + print("Failed to bulk update drivers") + print("Error code: "+str(response.status_code)) + print("Error response: "+response.text) + exit(1) print("Update drivers: ") print(drivers_updated) -print("\nDrivers currently deplpyed: ") +print("\nDrivers currently deployed: ") print(uploaded_drivers.keys()) From 0853fbbf7e18ef7372c291f52aa971a0c6c6b53b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 18 Sep 2025 11:50:47 -0700 Subject: [PATCH 135/449] WWSTCERT-8003 Decora Smart Wi-Fi (2nd Gen) Switch WWSTCERT-8007 Decora Smart Wi-Fi (2nd Gen) 600W Dimmer --- drivers/SmartThings/matter-switch/fingerprints.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index e5b25558a1..83d9073e85 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -586,6 +586,17 @@ matterManufacturer: vendorId: 0x1021 productId: 0x0006 deviceProfileName: switch-level +#Leviton + - id: "4251/4097" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Switch + vendorId: 0x109B + productId: 0x1001 + deviceProfileName: switch-binary + - id: "4251/4096" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) 600W Dimmer + vendorId: 0x109B + productId: 0x1000 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From d2fd209f9bc546b861f059cf6664a8e023309ecd Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:10:47 -0500 Subject: [PATCH 136/449] update Level Lock fingerprint to use batteryLevel (#2409) --- drivers/SmartThings/matter-lock/fingerprints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 441e3ae8ab..bd5cc13d0c 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -61,12 +61,12 @@ matterManufacturer: deviceLabel: Level Lock Pro vendorId: 0x129F productId: 0x0004 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock-nocodes-notamper-batteryLevel - id: "4767/5" deviceLabel: Level Lock (Matter) vendorId: 0x129F productId: 0x0005 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock-nocodes-notamper-batteryLevel #Nuki - id: "4957/161" deviceLabel: Nuki Smart Lock Ultra From 730ae40f5ddfc27c058e8a7fe29fd2d2f9877117 Mon Sep 17 00:00:00 2001 From: nicklpeterson Date: Fri, 19 Sep 2025 13:06:03 -0500 Subject: [PATCH 137/449] Update dry run boolean logic to handle boolean env variable --- tools/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deploy.py b/tools/deploy.py index 451229f9a4..e0d3afadbe 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,7 +5,7 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" -DRY_RUN = os.environ.get("DRY_RUN") != None +DRY_RUN = os.environ.get("DRY_RUN") == True or os.environ.get("DRY_RUN") == "True" print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) From 6a166ae6454addc184cdbf6ec4919d7389e166f3 Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Thu, 18 Sep 2025 14:30:11 -0500 Subject: [PATCH 138/449] Remove the adding of monitored attributes in Zigbee drivers This feature is already disabled on these drivers and is moving toward deprecation. We will remove the unnecessary adding --- drivers/SmartThings/zigbee-button/src/aqara/init.lua | 1 - drivers/SmartThings/zigbee-button/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-button/src/samjin/init.lua | 1 - .../zigbee-button/src/zigbee-multi-button/linxura/init.lua | 1 - .../src/zigbee-multi-button/zunzunbee/init.lua | 1 - drivers/SmartThings/zigbee-contact/src/aqara/init.lua | 1 - .../zigbee-contact/src/aurora-contact-sensor/init.lua | 1 - .../zigbee-contact/src/contact-temperature-sensor/init.lua | 1 - drivers/SmartThings/zigbee-contact/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-contact/src/init.lua | 1 - drivers/SmartThings/zigbee-contact/src/sengled/init.lua | 1 - .../CentraliteSystems/init.lua | 1 - drivers/SmartThings/zigbee-fan/src/init.lua | 1 - .../SmartThings/zigbee-humidity-sensor/src/aqara/init.lua | 1 - .../zigbee-humidity-sensor/src/frient-sensor/init.lua | 1 - drivers/SmartThings/zigbee-humidity-sensor/src/init.lua | 1 - .../zigbee-illuminance-sensor/src/aqara/init.lua | 1 - .../zigbee-lock/src/lock-without-codes/init.lua | 1 - .../SmartThings/zigbee-motion-sensor/src/aqara/init.lua | 1 - .../SmartThings/zigbee-motion-sensor/src/frient/init.lua | 7 ------- .../SmartThings/zigbee-motion-sensor/src/sengled/init.lua | 1 - drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua | 2 -- .../zigbee-power-meter/src/shinasystems/init.lua | 1 - drivers/SmartThings/zigbee-presence-sensor/src/init.lua | 1 - .../zigbee-smoke-detector/src/aqara-gas/init.lua | 1 - .../SmartThings/zigbee-smoke-detector/src/aqara/init.lua | 1 - .../SmartThings/zigbee-smoke-detector/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-switch/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-switch/src/init.lua | 1 - .../zigbee-switch/src/zigbee-dimming-light/init.lua | 1 - .../src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua | 1 - drivers/SmartThings/zigbee-thermostat/src/popp/init.lua | 1 - drivers/SmartThings/zigbee-valve/src/ezex/init.lua | 1 - .../zigbee-water-leak-sensor/src/aqara/init.lua | 1 - .../zigbee-water-leak-sensor/src/frient/init.lua | 1 - drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua | 1 - .../zigbee-water-leak-sensor/src/sengled/init.lua | 1 - .../src/aqara/curtain-driver-e1/init.lua | 1 - 38 files changed, 45 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 5fd1076b4a..5991fd4cb5 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -76,7 +76,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index fbe07eefac..e7df0e61c6 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -156,7 +156,6 @@ end local function init_handler(self, device) for _,attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end battery_defaults.enable_battery_voltage_table(device, battery_table) end diff --git a/drivers/SmartThings/zigbee-button/src/samjin/init.lua b/drivers/SmartThings/zigbee-button/src/samjin/init.lua index 66ae410169..116e6aaa80 100644 --- a/drivers/SmartThings/zigbee-button/src/samjin/init.lua +++ b/drivers/SmartThings/zigbee-button/src/samjin/init.lua @@ -21,7 +21,6 @@ battery_config.data_type = zcl_clusters.PowerConfiguration.attributes.BatteryVol local function init_handler(self, device) device:add_configured_attribute(battery_config) - device:add_monitored_attribute(battery_config) end local samjin_button = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua index 2be3564a93..0e7cf44e93 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua @@ -67,7 +67,6 @@ end local function device_init(driver, device) for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua index d5355c3b10..7bb209a622 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua @@ -38,7 +38,6 @@ local ZUNZUNBEE_BUTTON_FINGERPRINTS = { -- Initialize device attributes local function init_handler(self, device) device:add_configured_attribute(battery_config) - device:add_monitored_attribute(battery_config) end -- Check if a given device matches the supported fingerprints diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua index a952e0bfef..477a62afd4 100644 --- a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua @@ -53,7 +53,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua index 43002ec0ad..efa6fb8c37 100644 --- a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua @@ -47,7 +47,6 @@ local function device_init(driver, device) for _, attribute in ipairs(AURORA_CONTACT_CONFIGURATION) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua index 52319ad34d..33b2e3daa3 100644 --- a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua @@ -52,7 +52,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index 6e5ff2af00..21f2c88c77 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -44,7 +44,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-contact/src/init.lua b/drivers/SmartThings/zigbee-contact/src/init.lua index 7efb05179d..67be42e671 100644 --- a/drivers/SmartThings/zigbee-contact/src/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/init.lua @@ -65,7 +65,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua index afe1f29770..95b6e5e3e8 100644 --- a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua @@ -41,7 +41,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua index 0734bf57e8..77bc9fc5a6 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua @@ -98,7 +98,6 @@ local voltage_configuration = { local function device_init(driver, device) device:add_configured_attribute(voltage_configuration) - device:add_monitored_attribute(voltage_configuration) device:remove_monitored_attribute(zcl_clusters.PowerConfiguration.ID, zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:remove_configured_attribute(zcl_clusters.PowerConfiguration.ID, zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:set_field(battery_defaults.DEVICE_MIN_VOLTAGE_KEY, 2.3) diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index 1766be9ff1..a34e90ff5e 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -22,7 +22,6 @@ local device_init = function(self, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua index e712f7711f..5256bf47e2 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua @@ -67,7 +67,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index a56d070daa..7791e6752b 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -38,7 +38,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index 31dc36ce7b..83a14c70dc 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -51,7 +51,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua index a345d816c8..72fcbc2489 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua @@ -69,7 +69,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index 46f0867857..7272991459 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -38,7 +38,6 @@ local function device_init(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua index e5649dab4d..1746b21953 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua @@ -96,7 +96,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua index 43ce315ab2..c067b2fe56 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua @@ -109,24 +109,17 @@ local function device_init(driver, device) battery_defaults.build_linear_voltage_init(BATTERY_MIN_VOLTAGE, BATTERY_MAX_VOLTAGE)(driver, device) local attribute - attribute = CONFIGURATIONS[OCCUPANCY_ENDPOINT] - -- binding is directly triggered for specific endpoint in do_configure - device:add_monitored_attribute(attribute) - if device:supports_capability_by_id(capabilities.temperatureMeasurement.ID) then attribute = CONFIGURATIONS[TEMPERATURE_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end if device:supports_capability_by_id(capabilities.illuminanceMeasurement.ID) then attribute = CONFIGURATIONS[ILLUMINANCE_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end if device:supports_capability_by_id(capabilities.tamperAlert.ID) then attribute = CONFIGURATIONS[TAMPER_ENDPOINT] device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua index 6ae1de27df..0e2575415f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua @@ -41,7 +41,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua index 0b85da97c7..b0bf581748 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua @@ -49,8 +49,6 @@ end local device_init = function(self, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) - - device:add_monitored_attribute(instantaneous_demand_configuration) device:add_configured_attribute(instantaneous_demand_configuration) end diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua index 4ba1c6467e..3d28f5a0a5 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua @@ -111,7 +111,6 @@ local device_init = function(self, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) for _, attribute in ipairs(POWERMETER_CONFIGURATION_V2) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index d52722ed16..cc1611f2ec 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -111,7 +111,6 @@ end local function init_handler(self, device, event, args) device:set_field(battery_defaults.DEVICE_VOLTAGE_TABLE_KEY, battery_table) device:add_configured_attribute(battery_voltage_attr_configuration) - device:add_monitored_attribute(battery_voltage_attr_configuration) device:remove_monitored_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryPercentageRemaining.ID) device:remove_configured_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryPercentageRemaining.ID) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua index 863385accc..297701272d 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua @@ -138,7 +138,6 @@ local function device_init(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua index c9813a2b10..91aba0a5e9 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua @@ -112,7 +112,6 @@ local function device_init(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index 7bc2fe48b2..b882c3012a 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -81,7 +81,6 @@ local function device_init(driver, device) if CONFIGURATIONS ~= nil then for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-switch/src/frient/init.lua b/drivers/SmartThings/zigbee-switch/src/frient/init.lua index 4982fa038a..c075fa9320 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/init.lua @@ -139,7 +139,6 @@ local function do_configure(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end device:configure() diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index f1e6385f56..4d5295b3d0 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -78,7 +78,6 @@ local device_init = function(driver, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 153c8325e8..e74f544ad0 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -87,7 +87,6 @@ end local function device_init(driver, device) for _,attribute in ipairs(DIMMING_LIGHT_CONFIGURATION) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua index cf18d2ff82..ed6a24ba61 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua @@ -48,7 +48,6 @@ local device_init = function(self, device) if configuration ~= nil then for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end end diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua index bcd5a41dad..007fb722b0 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua @@ -325,7 +325,6 @@ local function device_init(driver, device) -- Add the manufacturer-specific attributes to generate their configure reporting and bind requests for _, config in pairs(cluster_configurations) do device:add_configured_attribute(config) - device:add_monitored_attribute(config) end -- initial set of heating mode local stored_heat_mode = device:get_field(STORED_HEAT_MODE) or 'eco' diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua index 53eddb5a1c..47ced66806 100644 --- a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua @@ -56,7 +56,6 @@ end local function device_init(driver, device) for _, attribute in ipairs(configuration) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua index 7ad385a13a..7319bae636 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua @@ -47,7 +47,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua index b85b58eb7b..5e92ef80d6 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua @@ -26,7 +26,6 @@ local function device_init(driver, device) battery_defaults.build_linear_voltage_init(config.minV, config.maxV)(driver, device) elseif (config.cluster) then device:add_configured_attribute(config) - device:add_monitored_attribute(config) end end end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua index 994d9d40e4..68a218a586 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua @@ -59,7 +59,6 @@ local function device_init(driver, device) battery_defaults.enable_battery_voltage_table(device, config.battery_voltage_table) elseif (config.cluster) then device:add_configured_attribute(config) - device:add_monitored_attribute(config) end end end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua index f34cd8181f..f00263e309 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua @@ -41,7 +41,6 @@ local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 3af5364bbe..bbbe24112e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -76,7 +76,6 @@ local CONFIGURATIONS = { local function device_init(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) - device:add_monitored_attribute(attribute) end end From 6fe2a8e4e8313386587a8ec231bb217954b06f63 Mon Sep 17 00:00:00 2001 From: Nick Peterson Date: Thu, 18 Sep 2025 18:24:55 +0000 Subject: [PATCH 139/449] Refactor Jenkins Pipeline (#2393) --- Jenkinsfile | 20 ++++++------------- tools/deploy.py | 51 +++++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 92d915bac5..62bfe12c65 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,13 +28,14 @@ pipeline { agent { docker { image 'python:3.10' - label 'production' + label "${params.NODE_LABEL ?: 'production'}" args '--entrypoint= -u 0:0' } } environment { BRANCH = getEnvName() CHANGED_DRIVERS = getChangedDrivers() + ENVIRONMENT = "${env.NODE_LABEL.toUpperCase()}" } stages { stage('requirements') { @@ -51,22 +52,13 @@ pipeline { } } stage('update') { - matrix { - axes { - axis { - name 'ENVIRONMENT' - values 'DEV', 'STAGING', 'ACCEPTANCE', 'PRODUCTION' - } - } - stages { - stage('environment_update') { - steps { - sh 'python3 tools/deploy.py' - } + stages { + stage('environment_update') { + steps { + sh 'python3 tools/deploy.py' } } } } } } - diff --git a/tools/deploy.py b/tools/deploy.py index c9d92448f5..451229f9a4 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,23 +5,23 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" +DRY_RUN = os.environ.get("DRY_RUN") != None print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) -branch_environment = "{}_{}_".format(BRANCH, ENVIRONMENT) -ENVIRONMENT_URL = os.environ.get(ENVIRONMENT+'_ENVIRONMENT_URL') +ENVIRONMENT_URL = os.environ.get('ENVIRONMENT_URL') if not ENVIRONMENT_URL: print("No environment url specified, aborting.") exit(0) UPLOAD_URL = ENVIRONMENT_URL+"/drivers/package" -CHANNEL_ID = os.environ.get(branch_environment+'CHANNEL_ID') +CHANNEL_ID = os.environ.get(BRANCH+'_CHANNEL_ID') if not CHANNEL_ID: - print("No channel id specified, aborting.") + print("No channel id specified for "+BRANCH+", aborting.") exit(0) UPDATE_URL = ENVIRONMENT_URL+"/channels/"+CHANNEL_ID+"/drivers/bulk" -TOKEN = os.environ.get(ENVIRONMENT+'_TOKEN') +TOKEN = os.environ.get('TOKEN') DRIVERID = "driverId" VERSION = "version" PACKAGEKEY = "packageKey" @@ -91,7 +91,8 @@ headers = { "Accept": "application/vnd.smartthings+json;v=20200810", "Authorization": "Bearer "+TOKEN, - "X-ST-LOG-LEVEL": "DEBUG" + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) }, json = { DRIVERID: driver[DRIVERID], @@ -147,7 +148,7 @@ print(error.stderr) retries += 1 if retries >= 5: - print("5 zip failires, skipping "+package_key+" and continuing.") + print("5 zip failures, skipping "+package_key+" and continuing.") continue with open(driver+".zip", 'rb') as driver_package: data = driver_package.read() @@ -187,23 +188,27 @@ print("Uploading package: {} driver id: {} version: {}".format(package_key, driver_info[DRIVERID], driver_info[VERSION])) driver_updates.append({DRIVERID: driver_info[DRIVERID], VERSION: driver_info[VERSION]}) -response = requests.put( - UPDATE_URL, - headers={ - "Accept": "application/vnd.smartthings+json;v=20200810", - "Authorization": "Bearer "+TOKEN, - "Content-Type": "application/json", - "X-ST-LOG-LEVEL": "DEBUG" - }, - data=json.dumps(driver_updates) -) -if response.status_code != 204: - print("Failed to bulk update drivers") - print("Error code: "+str(response.status_code)) - print("Error response: "+response.text) - exit(1) +if DRY_RUN: + print("Dry Run, skipping bulk upload to " + UPDATE_URL) +else: + response = requests.put( + UPDATE_URL, + headers={ + "Accept": "application/vnd.smartthings+json;v=20200810", + "Authorization": "Bearer "+TOKEN, + "Content-Type": "application/json", + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) + }, + data=json.dumps(driver_updates) + ) + if response.status_code != 204: + print("Failed to bulk update drivers") + print("Error code: "+str(response.status_code)) + print("Error response: "+response.text) + exit(1) print("Update drivers: ") print(drivers_updated) -print("\nDrivers currently deplpyed: ") +print("\nDrivers currently deployed: ") print(uploaded_drivers.keys()) From ffe4bc9e4bea047d598aedd5165e4b4c693a825a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 19 Sep 2025 11:09:38 -0700 Subject: [PATCH 140/449] Merge pull request #2413 from SmartThingsCommunity/np/jenkins-dry-run Update dry run boolean logic to handle boolean env variable --- tools/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deploy.py b/tools/deploy.py index 451229f9a4..e0d3afadbe 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,7 +5,7 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" -DRY_RUN = os.environ.get("DRY_RUN") != None +DRY_RUN = os.environ.get("DRY_RUN") == True or os.environ.get("DRY_RUN") == "True" print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) From 660809dd0e232c3700224c08592b71288776b3a6 Mon Sep 17 00:00:00 2001 From: Nick Peterson Date: Thu, 18 Sep 2025 18:24:55 +0000 Subject: [PATCH 141/449] Refactor Jenkins Pipeline (#2393) --- Jenkinsfile | 20 ++++++------------- tools/deploy.py | 51 +++++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 37 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 92d915bac5..62bfe12c65 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,13 +28,14 @@ pipeline { agent { docker { image 'python:3.10' - label 'production' + label "${params.NODE_LABEL ?: 'production'}" args '--entrypoint= -u 0:0' } } environment { BRANCH = getEnvName() CHANGED_DRIVERS = getChangedDrivers() + ENVIRONMENT = "${env.NODE_LABEL.toUpperCase()}" } stages { stage('requirements') { @@ -51,22 +52,13 @@ pipeline { } } stage('update') { - matrix { - axes { - axis { - name 'ENVIRONMENT' - values 'DEV', 'STAGING', 'ACCEPTANCE', 'PRODUCTION' - } - } - stages { - stage('environment_update') { - steps { - sh 'python3 tools/deploy.py' - } + stages { + stage('environment_update') { + steps { + sh 'python3 tools/deploy.py' } } } } } } - diff --git a/tools/deploy.py b/tools/deploy.py index c9d92448f5..451229f9a4 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,23 +5,23 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" +DRY_RUN = os.environ.get("DRY_RUN") != None print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) -branch_environment = "{}_{}_".format(BRANCH, ENVIRONMENT) -ENVIRONMENT_URL = os.environ.get(ENVIRONMENT+'_ENVIRONMENT_URL') +ENVIRONMENT_URL = os.environ.get('ENVIRONMENT_URL') if not ENVIRONMENT_URL: print("No environment url specified, aborting.") exit(0) UPLOAD_URL = ENVIRONMENT_URL+"/drivers/package" -CHANNEL_ID = os.environ.get(branch_environment+'CHANNEL_ID') +CHANNEL_ID = os.environ.get(BRANCH+'_CHANNEL_ID') if not CHANNEL_ID: - print("No channel id specified, aborting.") + print("No channel id specified for "+BRANCH+", aborting.") exit(0) UPDATE_URL = ENVIRONMENT_URL+"/channels/"+CHANNEL_ID+"/drivers/bulk" -TOKEN = os.environ.get(ENVIRONMENT+'_TOKEN') +TOKEN = os.environ.get('TOKEN') DRIVERID = "driverId" VERSION = "version" PACKAGEKEY = "packageKey" @@ -91,7 +91,8 @@ headers = { "Accept": "application/vnd.smartthings+json;v=20200810", "Authorization": "Bearer "+TOKEN, - "X-ST-LOG-LEVEL": "DEBUG" + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) }, json = { DRIVERID: driver[DRIVERID], @@ -147,7 +148,7 @@ print(error.stderr) retries += 1 if retries >= 5: - print("5 zip failires, skipping "+package_key+" and continuing.") + print("5 zip failures, skipping "+package_key+" and continuing.") continue with open(driver+".zip", 'rb') as driver_package: data = driver_package.read() @@ -187,23 +188,27 @@ print("Uploading package: {} driver id: {} version: {}".format(package_key, driver_info[DRIVERID], driver_info[VERSION])) driver_updates.append({DRIVERID: driver_info[DRIVERID], VERSION: driver_info[VERSION]}) -response = requests.put( - UPDATE_URL, - headers={ - "Accept": "application/vnd.smartthings+json;v=20200810", - "Authorization": "Bearer "+TOKEN, - "Content-Type": "application/json", - "X-ST-LOG-LEVEL": "DEBUG" - }, - data=json.dumps(driver_updates) -) -if response.status_code != 204: - print("Failed to bulk update drivers") - print("Error code: "+str(response.status_code)) - print("Error response: "+response.text) - exit(1) +if DRY_RUN: + print("Dry Run, skipping bulk upload to " + UPDATE_URL) +else: + response = requests.put( + UPDATE_URL, + headers={ + "Accept": "application/vnd.smartthings+json;v=20200810", + "Authorization": "Bearer "+TOKEN, + "Content-Type": "application/json", + "X-ST-LOG-LEVEL": "DEBUG", + "X-ST-CORRELATION": "driver-deployment-"+BRANCH+"-"+ENVIRONMENT+"-"+str(time.time()) + }, + data=json.dumps(driver_updates) + ) + if response.status_code != 204: + print("Failed to bulk update drivers") + print("Error code: "+str(response.status_code)) + print("Error response: "+response.text) + exit(1) print("Update drivers: ") print(drivers_updated) -print("\nDrivers currently deplpyed: ") +print("\nDrivers currently deployed: ") print(uploaded_drivers.keys()) From 0341df26fd98181896f4b03f18e0cbb04e6710e5 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 19 Sep 2025 11:09:38 -0700 Subject: [PATCH 142/449] Merge pull request #2413 from SmartThingsCommunity/np/jenkins-dry-run Update dry run boolean logic to handle boolean env variable --- tools/deploy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/deploy.py b/tools/deploy.py index 451229f9a4..e0d3afadbe 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -5,7 +5,7 @@ CHANGED_DRIVERS = os.environ.get('CHANGED_DRIVERS') # configurable from Jenkins to override and manually set the drivers to be uploaded DRIVERS_OVERRIDE = os.environ.get('DRIVERS_OVERRIDE') or "[]" -DRY_RUN = os.environ.get("DRY_RUN") != None +DRY_RUN = os.environ.get("DRY_RUN") == True or os.environ.get("DRY_RUN") == "True" print(BRANCH) print(ENVIRONMENT) print(CHANGED_DRIVERS) From f9fe54ec11255eaa39cb26eb4846353be1b23dbd Mon Sep 17 00:00:00 2001 From: Konrad K <33450498+KKlimczukS@users.noreply.github.com> Date: Fri, 19 Sep 2025 23:59:08 +0200 Subject: [PATCH 143/449] WWSTCERT-7585 Bosh plug compact +m (#2352) * adds Bosch Plug Compact [+M] * Plug Compact [M] --- 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 83d9073e85..a53d91cf16 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -131,6 +131,12 @@ matterManufacturer: vendorId: 0x1396 productId: 0x1001 deviceProfileName: light-color-level-fan +#Bosch Smart Home + - id: "4617/12310" + deviceLabel: Plug Compact [M] + vendorId: 0x1209 + productId: 0x3016 + deviceProfileName: plug-power-energy-powerConsumption #Chengdu - id: "5218/8197" deviceLabel: Magic Cube DS001 From 40262061d986f4d4c26dcec42b511d2a11b01fbd Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 19 Sep 2025 15:12:46 -0700 Subject: [PATCH 144/449] WWSTCERT-8033 LIFX Everyday Smart Light 2-Pack --- 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 a53d91cf16..f1c7b0f4cb 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -751,6 +751,11 @@ matterManufacturer: vendorId: 0x1423 productId: 0x0077 deviceProfileName: light-level-colorTemperature-1500k-9000k + - id: "5155/191" + deviceLabel: LIFX Everyday Smart Light 2-Pack + vendorId: 0x1423 + productId: 0x00BF + deviceProfileName: light-color-level #LG - id: "4142/8784" deviceLabel: LG Smart Button (1 Button) From 3b5ed9f15293b826328c6780d5210937fb50c231 Mon Sep 17 00:00:00 2001 From: janbajc <63789839+janbajc@users.noreply.github.com> Date: Sat, 20 Sep 2025 00:17:34 +0200 Subject: [PATCH 145/449] Add shelly wave motion driver (#2381) * Add shelly wave motion driver * Trigger CLA check * Update copyright year * Update drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml Co-authored-by: Steven Green * Remove unnecessary handlers * Delete drivers/SmartThings/zwave-sensor/src/shelly-wave-motion/init.lua * Update init.lua --------- Co-authored-by: Steven Green --- .../SmartThings/zwave-sensor/fingerprints.yml | 6 ++ .../profiles/shelly-wave-motion.yml | 59 +++++++++++++++++++ drivers/SmartThings/zwave-sensor/src/init.lua | 2 +- .../zwave-sensor/src/preferences.lua | 15 ++++- 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 0d04f7aff9..727f02a2d6 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -542,6 +542,12 @@ zwaveManufacturer: productType: 0x0000 productId: 0x0001 deviceProfileName: base-water + - id: shelly/wave/motion + deviceLabel: Shelly Wave Motion + manufacturerId: 0x0460 + productType: 0x0100 + productId: 0x0082 + deviceProfileName: shelly-wave-motion zwaveGeneric: - id: "GenericSensorAlarm" deviceLabel: Z-Wave Sensor diff --git a/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml b/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml new file mode 100644 index 0000000000..5bbeab8ee5 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/profiles/shelly-wave-motion.yml @@ -0,0 +1,59 @@ +name: shelly-wave-motion +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 10000] + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - name: "ledOpnClsChangeStat" + title: "P157: open/close change status" + description: "This parameter enables open/close status change by LED indicator." + required: false + preferenceType: enumeration + definition: + options: + 0 : "LED ind.disabled" + 1 : "LED ind.enabled" + default: 0 + - name: "sensitivity" + title: "P158: sensitivity" + description: "Sensitivity" + + required: false + preferenceType: enumeration + definition: + options: + 0 : "low sensitivity" + 1 : "moderate sensitivity" + 2 : "high sensitivity" + default: 0 + - name: "blindTime" + title: "P159: Motion Blind time" + description: "Blind time in seconds after last detected motion" + required: false + preferenceType: integer + definition: + minimum: 2 + maximum : 8 + default: 5 + - name: "motionNotdetRepT" + title: "P160:Motion not detect.rep.time" + description: "Time after last detected motion for device to send motion not detected" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum : 32767 + default: 30 \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 8ad801de9e..d92a09fe56 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -152,7 +152,7 @@ local driver_template = { lazy_load_if_possible("v1-contact-event"), lazy_load_if_possible("timed-tamper-clear"), lazy_load_if_possible("wakeup-no-poll"), - lazy_load_if_possible("apiv6_bugfix") + lazy_load_if_possible("apiv6_bugfix"), }, lifecycle_handlers = { added = added_handler, diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 48ee75d78e..4921a996bf 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -148,7 +148,20 @@ local devices = { ledLowBrightness = {parameter_number = 82, size = 2}, ledHighBrightness = {parameter_number = 83, size = 2} } - } + }, + SHELLY_WAVE_MOTION_SENSOR = { + MATCHING_MATRIX = { + mfrs = 0x0460, + product_types = 0x0100, + product_ids = {0x0082} + }, + PARAMETERS = { + ledOpnClsChangeStat = {parameter_number = 157, size = 1}, + sensitivity = {parameter_number = 158, size = 1}, + blindTime = {parameter_number = 159, size = 2}, + motionNotdetRepT = {parameter_number = 160, size = 2}, + }, + }, } local preferences = {} From 1730cbc16e73d40551e430f7f03db1de36f745ae Mon Sep 17 00:00:00 2001 From: Cornelius117 <72489457+Cornelius117@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:53:44 +0800 Subject: [PATCH 146/449] Add VID and PID of deasino product into new lock list (#2407) --- drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index f9c06f193c..7ae8a26e6f 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -78,6 +78,8 @@ local NEW_MATTER_LOCK_PRODUCTS = { {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 } From bdec9a45df78dbf57ed1cf6794fe2abbff5ccf5a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 19 Sep 2025 11:32:52 -0700 Subject: [PATCH 147/449] Merge pull request #2410 from SmartThingsCommunity/new_device/WWSTCERT-8003 WWSTCERT-8003 Decora Smart Wi-Fi (2nd Gen) Switch --- drivers/SmartThings/matter-switch/fingerprints.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index e5b25558a1..83d9073e85 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -586,6 +586,17 @@ matterManufacturer: vendorId: 0x1021 productId: 0x0006 deviceProfileName: switch-level +#Leviton + - id: "4251/4097" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Switch + vendorId: 0x109B + productId: 0x1001 + deviceProfileName: switch-binary + - id: "4251/4096" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) 600W Dimmer + vendorId: 0x109B + productId: 0x1000 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From e52978c78148986a9192e1ff6c3f93a05b694268 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 22 Sep 2025 12:03:25 -0700 Subject: [PATCH 148/449] Merge pull request #2419 from SmartThingsCommunity/cherry-pick/9-22-25 cherry pick to beta 9/22/25 --- drivers/SmartThings/matter-switch/fingerprints.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index e5b25558a1..83d9073e85 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -586,6 +586,17 @@ matterManufacturer: vendorId: 0x1021 productId: 0x0006 deviceProfileName: switch-level +#Leviton + - id: "4251/4097" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Switch + vendorId: 0x109B + productId: 0x1001 + deviceProfileName: switch-binary + - id: "4251/4096" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) 600W Dimmer + vendorId: 0x109B + productId: 0x1000 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From 9e61335ebc53049e7065e622d739dd2f2bee69bc Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 19 Aug 2025 10:34:31 -0500 Subject: [PATCH 149/449] sonos: Remove preemptive token refreshing. This does nothing without force refresh which could be problematic to use. --- drivers/SmartThings/sonos/src/sonos_driver.lua | 9 --------- 1 file changed, 9 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index c603a7f399..7fb7395047 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -25,8 +25,6 @@ if not security_load_success then security = nil end -local ONE_HOUR_IN_SECONDS = 3600 - ---@class SonosDriver: Driver --- ---@field public datastore table driver persistent store @@ -368,13 +366,6 @@ function SonosDriver:get_oauth_token() local now = os.time() -- token has not expired yet if now < expiration then - -- token is expiring soon, so we pre-emptively refresh - if math.abs(expiration - now) < ONE_HOUR_IN_SECONDS then - local result, err = security.get_sonos_oauth() - if not result then - log.warn(string.format("Error requesting OAuth token via Security API: %s", err)) - end - end return self.oauth.token else return nil, "token expired" From f9e285909124e95cd59e1f16d1a6ddc9d0a9a8c0 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:43:27 -0500 Subject: [PATCH 150/449] sonos: Add bus to receive oauth endpoint app info events --- drivers/SmartThings/sonos/src/sonos_driver.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 7fb7395047..0c3fa66f88 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -36,8 +36,8 @@ end ---@field public sonos SonosState Local state related to the sonos systems ---@field public discovery fun(driver: SonosDriver, opts: table, should_continue: fun(): boolean) ---@field private oauth_token_bus cosock.Bus.Sender bus for broadcasting new oauth tokens that arrive on the environment channel +---@field private oauth_info_bus cosock.Bus.Sender bus for broadcasting new endpoint app info that arrives on the environment channel ---@field private oauth { token: {accessToken: string, expiresAt: number}, endpoint_app_info: { state: "connected"|"disconnected" }, force_oauth: boolean? } cached OAuth info ----@field private waiting_for_oauth_token boolean ---@field private startup_state_received boolean ---@field private devices_waiting_for_startup_state SonosDevice[] --- @@ -87,9 +87,9 @@ end function SonosDriver:handle_augmented_data_change(update_key, decoded) if update_key == "endpointAppInfo" then self.oauth.endpoint_app_info = decoded + self.oauth_info_bus:send(decoded) elseif update_key == "sonosOAuthToken" then self.oauth.token = decoded - self.waiting_for_oauth_token = false self.oauth_token_bus:send(decoded) elseif update_key == "force_oauth" then self.oauth.force_oauth = decoded @@ -112,6 +112,15 @@ function SonosDriver:oauth_token_event_subscribe() return self.oauth_token_bus:subscribe() end +---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed +---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success +function SonosDriver:oauth_info_event_subscribe() + if api_version < 14 or security == nil then + return nil, "not supported" + end + return self.oauth_info_bus:subscribe() +end + function SonosDriver:update_after_startup_state_received() for k, v in pairs(self.hub_augmented_driver_data) do local decode_success, decoded = pcall(json.decode, v) @@ -127,9 +136,11 @@ function SonosDriver:handle_augmented_store_delete(update_key) if update_key == "endpointAppInfo" then log.trace "deleting endpoint app info" self.oauth.endpoint_app_info = nil + self.oauth_info_bus:send(nil) elseif update_key == "sonosOAuthToken" then log.trace "deleting OAuth Token" self.oauth.token = nil + self.oauth_token_bus:send(nil) elseif update_key == "force_oauth" then log.trace "deleting Force OAuth" self.oauth.force_oauth = nil @@ -613,11 +624,13 @@ end function SonosDriver.new_driver_template() local oauth_token_bus = cosock.bus() + local oauth_info_bus = cosock.bus() local template = { sonos = SonosState.instance(), discovery = SonosDisco.discover, oauth_token_bus = oauth_token_bus, + oauth_info_bus = oauth_info_bus, oauth = {}, waiting_for_oauth_token = false, startup_state_received = false, From 1861e4ea0371150cada4b53ade2ab3338001bc28 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:46:01 -0500 Subject: [PATCH 151/449] sonos: Only return oauth token if oauth is connected --- .../SmartThings/sonos/src/sonos_driver.lua | 35 +++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 0c3fa66f88..3afadee165 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -173,9 +173,7 @@ end ---@param update_key "endpointAppInfo"|"sonosOAuthToken" ---@param update_value string function SonosDriver:notify_augmented_data_changed(update_kind, update_key, update_value) - local already_connected = self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" + local already_connected = self:oauth_is_connected() log.info(string.format("Already connected? %s", already_connected)) if update_kind == "snapshot" then self:update_after_startup_state_received() @@ -235,12 +233,7 @@ end function SonosDriver:check_auth(info_or_device) local maybe_token, _ = self:get_oauth_token() - local token_valid = (api_version >= 14 and security ~= nil) - and self.oauth - and self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and maybe_token ~= nil - if token_valid then + if maybe_token then return true, SonosApi.api_keys.oauth_key elseif self.oauth.force_oauth then return false @@ -353,23 +346,14 @@ function SonosDriver:request_oauth_token() end ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ----@return "token expired"|"no token"|"not supported"|nil reason the reason a token was not provided, nil if there is a valid token available +---@return "token expired"|"no token"|"not supported"|"not connected"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() if api_version < 14 or security == nil then return nil, "not supported" end - self.hub_augmented_driver_data = self.hub_augmented_driver_data or {} - local decode_success, maybe_token = - pcall(json.decode, self.hub_augmented_driver_data.sonosOAuthToken) - if - decode_success - and type(maybe_token) == "table" - and type(maybe_token.accessToken) == "string" - and type(maybe_token.expiresAt) == "number" - then - self.oauth.token = maybe_token - elseif self.hub_augmented_driver_data.sonosOAuthToken ~= nil then - log.warn(string.format("Unable to JSON decode token from hub augmented data: %s", maybe_token)) + + if not self:oauth_is_connected() then + return nil, "not connected" end if self.oauth.token then @@ -386,6 +370,13 @@ function SonosDriver:get_oauth_token() return nil, "no token" end +function SonosDriver:oauth_is_connected() + return (api_version >= 14 and security ~= nil) + and self.oauth + and self.oauth.endpoint_app_info + and self.oauth.endpoint_app_info.state == "connected" +end + ---Create a cosock task that handles events from the persistent SSDP task. ---@param driver SonosDriver ---@param discovery_event_subscription cosock.Bus.Subscription From 64dfbedfd0891a362aaf86595d6d8eeda4a93886 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:48:21 -0500 Subject: [PATCH 152/449] sonos: Add persistent token refresher task --- .../SmartThings/sonos/src/sonos_driver.lua | 4 + .../SmartThings/sonos/src/token_refresher.lua | 152 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 drivers/SmartThings/sonos/src/token_refresher.lua diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 3afadee165..8247b21f45 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -206,6 +206,10 @@ end function SonosDriver:handle_startup_state_received() self:start_ssdp_event_task() self:notify_augmented_data_changed "snapshot" + if api_version >= 14 and security ~= nil then + local token_refresher = require "token_refresher" + token_refresher.spawn_token_refresher(self) + end self.startup_state_received = true for _, device in pairs(self.devices_waiting_for_startup_state) do SonosDriverLifecycleHandlers.initialize_device(self, device) diff --git a/drivers/SmartThings/sonos/src/token_refresher.lua b/drivers/SmartThings/sonos/src/token_refresher.lua new file mode 100644 index 0000000000..0734347795 --- /dev/null +++ b/drivers/SmartThings/sonos/src/token_refresher.lua @@ -0,0 +1,152 @@ +local cosock = require "cosock" +local utils = require "utils" +local security = require "st.security" +local log = require "log" + +local module = {} + +local ACTIONS = { + -- This action waits for an event via the oauth endpoint app info bus in order to determine + -- when the driver goes from a disconnected to connected state. + WAIT_FOR_CONNECTED = 1, + -- This action will wait for the current valid token to expire. It will also handle a new token + -- event to redetermine the current action. New tokens that come in during this action will likely + -- be from debug testing. + WAIT_FOR_EXPIRE = 2, + -- This action requests a new token and waits for it to come in. + REQUEST_TOKEN = 3, +} + +local ACTION_STRINGIFY = { + [ACTIONS.WAIT_FOR_CONNECTED] = "wait for connected", + [ACTIONS.WAIT_FOR_EXPIRE] = "wait for expire", + [ACTIONS.REQUEST_TOKEN] = "request token", +} + +local Refresher = {} +Refresher.__index = Refresher + +--- Determine which action the refresher should take. +--- This just depends on: +--- - Is Oauth connected? +--- - Do we have a valid token? +function Refresher:determine_action() + if not self.driver:oauth_is_connected() then + -- Oauth is disconnected so no point in trying to request a token until we are connected. + return ACTIONS.WAIT_FOR_CONNECTED + end + local token, _ = self.driver:get_oauth_token() + if token then + local now = os.time() + local expiration = math.floor(token.expiresAt / 1000) + if (expiration - now) > 60 then + -- Token is valid and not expiring in the next 60 seconds. + return ACTIONS.WAIT_FOR_EXPIRE + end + end + -- We don't have a valid token or it is about to expire soon. + return ACTIONS.REQUEST_TOKEN +end + +--- Waits for a token event with a timeout. +--- @param timeout number How long the function will wait for a new token +function Refresher:try_wait_for_token_event(timeout) + local token_bus, err = self.driver:oauth_token_event_subscribe() + if err == "closed" then + self.token_bus_closed = true + end + if token_bus then + token_bus:settimeout(timeout) + token_bus:receive() + end +end + +--- Waits for the current token to expire or a new token event. +--- +--- The likely outcome of this function is to wait the entire expiration timeout. It will +--- also listen for token events just in case a new token with a new expiration is sent to the driver. +--- A new token would most likely come from developer testing, but since the new token requests are +--- not synchronous one could come from an earlier request. +function Refresher:wait_for_expire_or_token_event() + local maybe_token, err = self.driver:get_oauth_token() + if not maybe_token then + -- Something got funky in the state machine, return and re-determine our next action + log.warn(string.format("Tried to wait for expiration of non-existent token: %s", err)) + return + end + -- The token will be refreshed if requested within 1 minute of expiration + local expiration = math.floor(maybe_token.expiresAt / 1000) - 60 + local now = os.time() + local timeout = math.max(expiration - now, 0) + + log.debug(string.format("Token will refresh in %d seconds", timeout)) + -- Wait while trying to receive a token event in case it gets updated for some reason. + self:try_wait_for_token_event(timeout) +end + +--- Waits for an oauth endpoint app info event indefinitely. +--- +--- A new info event indicates that `Refresher:determine_action` should be called to check if oauth +--- is now connected. +function Refresher:wait_for_info_event() + local info_sub, err = self.driver:oauth_info_event_subscribe() + if err == "closed" then + self.info_bus_closed = true + end + if info_sub then + info_sub:receive() + end +end + +--- Requests a token then waits for a new token event. +function Refresher:request_token() + local result, err = security.get_sonos_oauth() + if not result then + log.warn(string.format("Failed to request oauth token: %s", err)) + end + -- Try to receive token even if the request failed. + self:try_wait_for_token_event(10) + local maybe_token, _ = self.driver:get_oauth_token() + if maybe_token then + -- token is valid, reset backoff + self.token_backoff = utils.backoff_builder(30 * 60, 5, 0.1) + else + -- We either didn't receive a token or it is not valid. + -- Backoff and maybe we will receive it in that time, or we retry. + cosock.socket.sleep(self.token_backoff()) + end +end + +function module.spawn_token_refresher(driver) + local refresher = setmetatable({ driver = driver, + token_backoff = utils.backoff_builder(30 * 60, 5, 0.1), + }, + Refresher) + cosock.spawn(function () + while true do + -- We can always determine what we should be doing based off the information we have, + -- any action can proceed action depending on what needs to be done. + local action = refresher:determine_action() + log.info(string.format("Token refresher action: %s", ACTION_STRINGIFY[action])) + if action == ACTIONS.WAIT_FOR_CONNECTED then + refresher:wait_for_info_event() + elseif action == ACTIONS.WAIT_FOR_EXPIRE then + refresher:wait_for_expire_or_token_event() + elseif action == ACTIONS.REQUEST_TOKEN then + refresher:request_token() + else + log.error(string.format("Token refresher task exiting due to bad token refresher action: %s", action)) + return + end + if refresher.token_bus_closed or refresher.info_bus_closed then + log.error(string.format("Token refresher task exiting. Token bus closed: %s Info bus close: %s", + refresher.token_bus_closed, refresher.info_bus_closed)) + return + end + end + end, "token refresher task") +end + +return module + + From ad6a179137e7c01f47535729b64426490d6b2e1c Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:50:20 -0500 Subject: [PATCH 153/449] sonos: remove token requests outside of the token refresher task --- .../sonos/src/api/sonos_connection.lua | 21 +++------- .../sonos/src/lifecycle_handlers.lua | 19 ---------- .../SmartThings/sonos/src/sonos_driver.lua | 38 ------------------- 3 files changed, 5 insertions(+), 73 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index f1773e24aa..2649c4600e 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -242,11 +242,7 @@ end ---@param sonos_conn SonosConnection local function _oauth_reconnect_task(sonos_conn) log.debug("Spawning reconnect task for ", sonos_conn.device.label) - local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - local unauthorized = (check_auth == false) - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end + -- Subscribe first so we get all updates on the bus local token_receive_handle, err = sonos_conn.driver:oauth_token_event_subscribe() if not token_receive_handle then log.warn(string.format("error creating oauth token receive handle for respawn task: %s", err)) @@ -254,7 +250,10 @@ local function _oauth_reconnect_task(sonos_conn) cosock.spawn(function() local backoff = backoff_builder(60, 1, 0.1) while not sonos_conn:is_running() do - if sonos_conn.driver:is_waiting_for_oauth_token() and token_receive_handle then + local check_auth = sonos_conn.driver:check_auth(sonos_conn.device) + local unauthorized = (check_auth == false) + + if unauthorized then local token, channel_error = token_receive_handle:receive() if not token then log.warn(string.format("Error requesting token: %s", channel_error)) @@ -271,12 +270,6 @@ local function _oauth_reconnect_task(sonos_conn) end end - check_auth = sonos_conn.driver:check_auth(sonos_conn.device) - unauthorized = (check_auth == false) - - if unauthorized and not sonos_conn.driver:is_waiting_for_oauth_token() then - sonos_conn.driver:request_oauth_token() - end cosock.socket.sleep(backoff()) end sonos_conn._reconnecting = false @@ -339,10 +332,6 @@ function SonosConnection.new(driver, device) device.log.warn( string.format("WebSocket connection no longer authorized, disconnecting") ) - local _, security_err = driver:request_oauth_token() - if security_err then - log.warn(string.format("Error during request for oauth token: %s", security_err)) - end -- closing the socket directly without calling `:stop()` triggers the reconnect loop, -- which is where we wait for the token to come in. local unique_key, bad_key_part = utils.sonos_unique_key(household_id, player_id) diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 747381f798..46e874a1a2 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -82,16 +82,9 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) local token, token_recv_err -- max 30 mins local backoff_builder = utils.backoff_builder(60 * 30, 30, 2) - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn(string.format("Error sending token request: %s", request_token_err)) - end - end local backoff_timer = nil while not token do - local send_request = false -- we use the backoff to create a timer and utilize a select loop here, instead of -- utilizing a sleep, so that we can create a long delay on our polling of the cloud -- without putting ourselves in a situation where we're sleeping for an extended period @@ -117,7 +110,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) -- -- This is just in case both receivers are ready, so that we can prioritize -- handling the token instead of putting another request in flight. - send_request = true backoff_timer:handled() backoff_timer = nil end @@ -137,17 +129,6 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) ) ) end - - if send_request then - if not driver:is_waiting_for_oauth_token() then - local _, request_token_err = driver:request_oauth_token() - if request_token_err then - log.warn( - string.format("Error sending token request: %s", request_token_err) - ) - end - end - end end else device.log.error( diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 8247b21f45..5a483eda2b 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -98,11 +98,6 @@ function SonosDriver:handle_augmented_data_change(update_key, decoded) end end ----@return boolean -function SonosDriver:is_waiting_for_oauth_token() - return (api_version >= 14 and security ~= nil) and self.waiting_for_oauth_token -end - ---@return (cosock.Bus.Subscription)? receiver the subscription receiver if the bus hasn't been closed, nil if closed ---@return nil|"not supported"|"closed" err_msg "not supported" on old API versions, "closed" if the bus is closed, nil on success function SonosDriver:oauth_token_event_subscribe() @@ -190,17 +185,6 @@ function SonosDriver:notify_augmented_data_changed(update_kind, update_key, upda ) ) end - - if - self.oauth.endpoint_app_info - and self.oauth.endpoint_app_info.state == "connected" - and not already_connected - then - local _, err = self:request_oauth_token() - if err then - log.error(string.format("Request OAuth token error: %s", err)) - end - end end function SonosDriver:handle_startup_state_received() @@ -328,27 +312,6 @@ function SonosDriver:check_auth(info_or_device) ) end ----@return any? ret nil on permissions violation ----@return string? error nil on success -function SonosDriver:request_oauth_token() - if api_version < 14 or security == nil then - return nil, "not supported" - end - local maybe_token, maybe_err = self:get_oauth_token() - if maybe_err then - log.warn(string.format("get oauth token error: %s", maybe_err)) - end - if type(maybe_token) == "table" and type(maybe_token.accessToken) == "string" then - self.oauth_token_bus:send(maybe_token) - end - local result, err = security.get_sonos_oauth() - if not result then - return nil, string.format("Error requesting OAuth token via Security API: %s", err) - end - self.waiting_for_oauth_token = true - return result, err -end - ---@return { accessToken: string, expiresAt: number }? the token if a currently valid token is available, nil if not ---@return "token expired"|"no token"|"not supported"|"not connected"|nil reason the reason a token was not provided, nil if there is a valid token available function SonosDriver:get_oauth_token() @@ -627,7 +590,6 @@ function SonosDriver.new_driver_template() oauth_token_bus = oauth_token_bus, oauth_info_bus = oauth_info_bus, oauth = {}, - waiting_for_oauth_token = false, startup_state_received = false, devices_waiting_for_startup_state = {}, dni_to_device_id = utils.new_mac_address_keyed_table(), From b68520ebb99e6c9497b8053c0b90549bc97b4638 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Thu, 21 Aug 2025 10:51:10 -0500 Subject: [PATCH 154/449] sonos: Wait for valid token before sending commands --- .../sonos/src/api/cmd_handlers.lua | 8 +++--- .../SmartThings/sonos/src/sonos_driver.lua | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua index c2de236044..b5ccfe10b9 100644 --- a/drivers/SmartThings/sonos/src/api/cmd_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/cmd_handlers.lua @@ -24,9 +24,9 @@ local function _do_send_to_group(driver, device, payload) local household_id, group_id = driver.sonos:get_group_for_device(device) payload[1].householdId = household_id payload[1].groupId = group_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then @@ -40,9 +40,9 @@ local function _do_send_to_self(driver, device, payload) local household_id, player_id = driver.sonos:get_player_for_device(device) payload[1].householdId = household_id payload[1].playerId = player_id - local maybe_token, err = driver:get_oauth_token() + local maybe_token, err = driver:wait_for_oauth_token(30) if err then - log.warn(string.format("notice: get_oauth_token -> %s", err)) + log.warn(string.format("notice: wait_for_oauth_token -> %s", err)) end if maybe_token then diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 5a483eda2b..560874ddf8 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -337,6 +337,34 @@ function SonosDriver:get_oauth_token() return nil, "no token" end +function SonosDriver:wait_for_oauth_token(timeout) + if api_version < 14 or security == nil then + return nil, "not supported" + end + + if not self:oauth_is_connected() then + return nil, "not connected" + end + + -- See if a valid token is already available + local maybe_token, _ = self:get_oauth_token() + if maybe_token then + -- return the valid token + return maybe_token + end + -- Subscribe to the token event bus. A new token has been/will be requested + -- by the token refresher task. + local token_bus, err = self:oauth_token_event_subscribe() + if token_bus then + token_bus:settimeout(timeout) + -- Wait for the new token to come in + token_bus:receive() + -- Call `SonosDriver:get_oauth_token` again to ensure the token is valid. + return self:get_oauth_token() + end + return nil, err +end + function SonosDriver:oauth_is_connected() return (api_version >= 14 and security ~= nil) and self.oauth From 7a891bce3f87729d7792dc46e43901c82f680502 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 2 Sep 2025 09:54:11 -0500 Subject: [PATCH 155/449] sonos: Update oauth_is_connected to oauth_app_connected --- drivers/SmartThings/sonos/src/sonos_driver.lua | 8 ++++---- drivers/SmartThings/sonos/src/token_refresher.lua | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 560874ddf8..3fab6bcc04 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -168,7 +168,7 @@ end ---@param update_key "endpointAppInfo"|"sonosOAuthToken" ---@param update_value string function SonosDriver:notify_augmented_data_changed(update_kind, update_key, update_value) - local already_connected = self:oauth_is_connected() + local already_connected = self:oauth_app_connected() log.info(string.format("Already connected? %s", already_connected)) if update_kind == "snapshot" then self:update_after_startup_state_received() @@ -319,7 +319,7 @@ function SonosDriver:get_oauth_token() return nil, "not supported" end - if not self:oauth_is_connected() then + if not self:oauth_app_connected() then return nil, "not connected" end @@ -342,7 +342,7 @@ function SonosDriver:wait_for_oauth_token(timeout) return nil, "not supported" end - if not self:oauth_is_connected() then + if not self:oauth_app_connected() then return nil, "not connected" end @@ -365,7 +365,7 @@ function SonosDriver:wait_for_oauth_token(timeout) return nil, err end -function SonosDriver:oauth_is_connected() +function SonosDriver:oauth_app_connected() return (api_version >= 14 and security ~= nil) and self.oauth and self.oauth.endpoint_app_info diff --git a/drivers/SmartThings/sonos/src/token_refresher.lua b/drivers/SmartThings/sonos/src/token_refresher.lua index 0734347795..2ba3ad660e 100644 --- a/drivers/SmartThings/sonos/src/token_refresher.lua +++ b/drivers/SmartThings/sonos/src/token_refresher.lua @@ -31,7 +31,7 @@ Refresher.__index = Refresher --- - Is Oauth connected? --- - Do we have a valid token? function Refresher:determine_action() - if not self.driver:oauth_is_connected() then + if not self.driver:oauth_app_connected() then -- Oauth is disconnected so no point in trying to request a token until we are connected. return ACTIONS.WAIT_FOR_CONNECTED end From 03133d9029ecac4f1943206aa8fc72fd31f446e5 Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Thu, 11 Sep 2025 12:55:30 -0500 Subject: [PATCH 156/449] Update xCREAS Air Purifier fingerprint to static profile (#2390) --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 1091984b73..7015cc6431 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -66,7 +66,7 @@ matterManufacturer: deviceLabel: xCREAS Smart Air Purifier Cyclone400 vendorId: 0x156A productId: 0x0001 - deviceProfileName: air-purifier-modular + deviceProfileName: air-purifier-hepa-wind-aqs-pm25-meas-pm25-level matterGeneric: - id: "matter/hvac/heatcool" deviceLabel: Matter Thermostat From 77594aee6eac2a369026430252056caa2aa996cb Mon Sep 17 00:00:00 2001 From: GAFfrient <96058156+GAFfrient@users.noreply.github.com> Date: Fri, 12 Sep 2025 20:52:30 +0200 Subject: [PATCH 157/449] WWSTCERT-7636 Frient humidity sensor (hmszb 120/hmszb 110) add support (#2356) * Add support for HMSZB-120/HMSZB-110 * According to @greens feedback. * Whitespace * Updated test message. * Fix info_changed part. * Whitespace. * Fix test. * Fix test 2 * Whitespace fix. --------- Co-authored-by: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> --- .../zigbee-humidity-sensor/fingerprints.yml | 9 +- .../frient-humidity-temperature-battery.yml | 39 +++++ .../src/configurations.lua | 21 ++- .../src/frient-sensor/init.lua | 34 ++++- .../src/test/test_frient_sensor.lua | 136 ++++++++++++++++-- 5 files changed, 214 insertions(+), 25 deletions(-) create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index c6dd3efcca..9ab5af08d6 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -59,10 +59,15 @@ zigbeeManufacturer: model: HT-EF-3.0 deviceProfileName: humidity-temp-battery - id: frient/HMSZB-110 - deviceLabel: frient Multipurpose Sensor + deviceLabel: frient Humidity Sensor manufacturer: frient A/S model: HMSZB-110 - deviceProfileName: humidity-temp-battery + deviceProfileName: frient-humidity-temperature-battery + - id: frient/HMSZB-120 + deviceLabel: frient Humidity Sensor + manufacturer: frient A/S + model: HMSZB-120 + deviceProfileName: frient-humidity-temperature-battery - id: eWeLink/TH01 deviceLabel: eWeLink Multipurpose Sensor manufacturer: eWeLink diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml new file mode 100644 index 0000000000..cd186812bf --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-humidity-temperature-battery.yml @@ -0,0 +1,39 @@ +name: frient-humidity-temperature-battery +components: +- id: main + capabilities: + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: TempHumiditySensor +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index 5c8f6834e9..3a4e495e3e 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -22,24 +22,33 @@ local PowerConfiguration = clusters.PowerConfiguration local devices = { FRIENT_HUMIDITY_TEMP_SENSOR = { FINGERPRINTS = { - { mfr = "frient A/S", model = "HMSZB-110" } + { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" } }, CONFIGURATION = { { cluster = RelativeHumidity.ID, attribute = RelativeHumidity.attributes.MeasuredValue.ID, minimum_interval = 60, - maximum_interval = 600, + maximum_interval = 3600, data_type = RelativeHumidity.attributes.MeasuredValue.base_type, - reportable_change = 100 + reportable_change = 300 }, { cluster = TemperatureMeasurement.ID, attribute = TemperatureMeasurement.attributes.MeasuredValue.ID, - minimum_interval = 60, - maximum_interval = 600, + minimum_interval = 30, + maximum_interval = 3600, data_type = TemperatureMeasurement.attributes.MeasuredValue.base_type, - reportable_change = 10 + reportable_change = 100 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30 , + maximum_interval = 21600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 } } }, diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index c63b319cea..a56d070daa 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -14,9 +14,13 @@ local battery_defaults = require "st.zigbee.defaults.battery_defaults" local configurationMap = require "configurations" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" } } local function can_handle_frient_sensor(opts, driver, device) @@ -39,10 +43,36 @@ local function device_init(driver, device) end end +local function do_configure(driver, device, event, args) + device:configure() + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + for name, value in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local sensitivity = math.floor((device.preferences[name]) * 100 + 0.5) + if (name == "temperatureSensitivity") then + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, sensitivity)) + end + if (name == "humiditySensitivity") then + device:send(HumidityMeasurement.attributes.MeasuredValue:configure_reporting(device, 60, 3600, sensitivity)) + end + end + end + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + local frient_sensor = { NAME = "Frient Humidity Sensor", lifecycle_handlers = { - init = device_init + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed }, can_handle = can_handle_frient_sensor } 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 e61ae9e22d..c2723d0edd 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 @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. @@ -16,21 +16,22 @@ 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 TemperatureMeasurement = clusters.TemperatureMeasurement -local RelativeHumidity = clusters.RelativeHumidity +local HumidityMeasurement = clusters.RelativeHumidity local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("humidity-temp-battery.yml"), + profile = t_utils.get_profile_definition("frient-humidity-temperature-battery.yml"), zigbee_endpoints = { - [1] = { - id = 1, + [26] = { + id = 26, manufacturer = "frient A/S", - model = "HMSZB-110", + model = "HMSZB-120", server_clusters = {0x0001, 0x0402, 0x0405} - } + }, } } ) @@ -63,7 +64,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - RelativeHumidity.attributes.MeasuredValue:read(mock_device) + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) } }, { @@ -80,6 +81,38 @@ test.register_message_test( } ) +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + test.register_coroutine_test( "Configure should configure all necessary attributes", function() @@ -89,7 +122,7 @@ test.register_coroutine_test( mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, - RelativeHumidity.ID + HumidityMeasurement.ID ) }) test.socket.zigbee:__expect_send({ @@ -108,7 +141,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 100) + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -116,13 +149,86 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 600, 10) + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) }) - test.socket.zigbee:__expect_send({ mock_device.id, RelativeHumidity.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) -test.run_registered_tests() +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + end +) + +test.run_registered_tests() \ No newline at end of file From 70a778aab38299a0d4db78c0f90c14f036213a7b Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Fri, 19 Sep 2025 18:07:44 +0800 Subject: [PATCH 158/449] This commit fixes the issue of some devices "false alarm" after hub switch-over. We add check_latest state before sending initial event(open/unlocked/presense) for following drivers: - SmartThings/zigbee-button/src/aqara/init.lua - SmartThings/zigbee-button/src/dimming-remote/init.lua - SmartThings/zigbee-button/src/frient/init.lua - SmartThings/zigbee-button/src/init.lua - SmartThings/zigbee-button/src/iris/init.lua - SmartThings/zigbee-button/src/pushButton/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua - SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua - SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua - SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua - SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua - SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua - SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua - SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua - SmartThings/zigbee-window-treatment/src/aqara/init.lua - SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua - SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua - Unofficial/tuya-zigbee/src/button/init.lua - Unofficial/tuya-zigbee/src/button/meian-button/init.lua - Unofficial/tuya-zigbee/src/curtain/init.lua --- .../zigbee-button/src/aqara/init.lua | 3 +- .../zigbee-button/src/button_utils.lua | 6 +++ .../zigbee-button/src/dimming-remote/init.lua | 2 +- .../zigbee-button/src/frient/init.lua | 3 +- .../SmartThings/zigbee-button/src/init.lua | 3 +- .../zigbee-button/src/iris/init.lua | 2 +- .../zigbee-button/src/pushButton/init.lua | 3 +- .../src/test/test_aqara_button.lua | 12 +++++ .../src/test/test_dimming_remote.lua | 45 +++++++++++++++- .../src/test/test_frient_button.lua | 17 +++--- .../src/test/test_ikea_on_off.lua | 43 +++++++++++++++ .../src/test/test_ikea_open_close.lua | 42 +++++++++++++++ .../src/test/test_ikea_remote_control.lua | 52 ++++++++++++++++-- .../src/test/test_iris_button.lua | 21 +++++++- .../src/test/test_robb_4x_button.lua | 52 ++++++++++++++++++ .../src/test/test_robb_8x_button.lua | 53 +++++++++++++++++++ .../ikea/TRADFRI_remote_control.lua | 2 +- .../src/zigbee-multi-button/ikea/init.lua | 5 +- .../src/zigbee-multi-button/init.lua | 3 +- .../src/zigbee-multi-button/robb/init.lua | 3 +- .../src/zigbee-accessory-dimmer/init.lua | 13 +++-- .../zigbee-battery-accessory-dimmer/init.lua | 8 ++- .../src/aqara/multi-switch/init.lua | 3 +- .../src/inovelli-vzm31-sn/init.lua | 11 ++-- .../zigbee-switch/src/switch_utils.lua | 23 ++++++++ .../src/test/test_aqara_switch_no_power.lua | 19 ++++++- .../src/zigbee-dimming-light/init.lua | 3 +- .../src/aqara/curtain-driver-e1/init.lua | 5 +- .../src/aqara/init.lua | 5 +- .../src/aqara/roller-shade/init.lua | 5 +- .../test_zigbee_window_treatment_aqara.lua | 32 +++++++++++ ...ndow_treatment_aqara_curtain_driver_e1.lua | 19 +++++++ ...ow_treatment_aqara_roller_shade_rotate.lua | 23 ++++++++ .../src/window_treatment_utils.lua | 23 ++++++++ .../src/eaton-anyplace-switch/init.lua | 4 +- .../zwave-switch/src/switch_utils.lua | 23 ++++++++ .../src/test/test_eaton_anyplace_switch.lua | 34 ++++++++++++ .../tuya-zigbee/src/button/init.lua | 3 +- .../src/button/meian-button/init.lua | 3 +- .../tuya-zigbee/src/curtain/init.lua | 4 +- .../src/test/test_meian_button.lua | 21 ++++++++ .../tuya-zigbee/src/test/test_tuya_button.lua | 15 ++++++ .../src/test/test_tuya_curtain.lua | 10 ++++ .../Unofficial/tuya-zigbee/src/tuya_utils.lua | 6 +++ 44 files changed, 640 insertions(+), 47 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/switch_utils.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua create mode 100644 drivers/SmartThings/zwave-switch/src/switch_utils.lua diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 5fd1076b4a..1bb6eb0179 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -17,6 +17,7 @@ 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 button_utils = require "button_utils" local PowerConfiguration = clusters.PowerConfiguration @@ -84,7 +85,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:emit_event(capabilities.battery.battery(100)) end diff --git a/drivers/SmartThings/zigbee-button/src/button_utils.lua b/drivers/SmartThings/zigbee-button/src/button_utils.lua index b7499684cd..4a8f7101e0 100644 --- a/drivers/SmartThings/zigbee-button/src/button_utils.lua +++ b/drivers/SmartThings/zigbee-button/src/button_utils.lua @@ -79,4 +79,10 @@ button_utils.build_button_handler = function(button_name, pressed_type) end end +button_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + return button_utils diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua index 45809d728c..d08c975632 100644 --- a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua @@ -54,7 +54,7 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons}, {visibility = { displayed = false }})) end device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function do_configure(self, device) diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index fbe07eefac..722d98fa02 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -17,6 +17,7 @@ local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local data_types = require "st.zigbee.data_types" +local button_utils = require "button_utils" local BasicInput = zcl_clusters.BasicInput local PowerConfiguration = zcl_clusters.PowerConfiguration local OnOff = zcl_clusters.OnOff @@ -164,7 +165,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function do_configure(driver, device, event, args) diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index 776eaa8b9e..a61ff415a7 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -18,6 +18,7 @@ local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" local IASZone = (require "st.zigbee.zcl.clusters").IASZone local TemperatureMeasurement = (require "st.zigbee.zcl.clusters").TemperatureMeasurement +local button_utils = require "button_utils" local temperature_measurement_defaults = { MIN_TEMP = "MIN_TEMP", @@ -109,7 +110,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) if device:supports_server_cluster(TemperatureMeasurement.ID) then device:send(TemperatureMeasurement.attributes.MaxMeasuredValue:read(device)) device:send(TemperatureMeasurement.attributes.MinMeasuredValue:read(device)) diff --git a/drivers/SmartThings/zigbee-button/src/iris/init.lua b/drivers/SmartThings/zigbee-button/src/iris/init.lua index 1baa2f8ced..157e766748 100644 --- a/drivers/SmartThings/zigbee-button/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-button/src/iris/init.lua @@ -62,7 +62,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) end diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua index 8d09684fd7..6fec59d69e 100644 --- a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua +++ b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua @@ -24,11 +24,12 @@ -- for the specific language governing permissions and limitations under the License. local capabilities = require "st.capabilities" +local button_utils = require "button_utils" local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local push_button = { 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 ba0f87741f..b53a966761 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -67,22 +67,34 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle -- e1", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) end ) test.register_coroutine_test( "Handle added lifecycle -- t1", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) end ) 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 32e686a9f5..4ba87cb461 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua @@ -176,6 +176,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -221,7 +222,49 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + 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( + "button1", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 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 ef20adc678..3df15b9f08 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -165,18 +165,23 @@ test.register_coroutine_test( [15] = 0, [10] = 0 } + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) 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.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) test.wait_for_events() - - for voltage, batt_perc in pairs(battery_table) do - test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc)) ) - - end + for voltage, batt_perc in pairs(battery_table) do + test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc))) + end + test.wait_for_events() + -- Avoid sending the button pushed contactSensor 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.supportedButtonValues({"pushed"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.wait_for_events() end ) 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 f685d6fbec..9479358793 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 @@ -211,6 +211,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -251,6 +252,48 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed 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.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ 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 ec5cb8a1dc..7b41684c5a 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 @@ -155,6 +155,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -195,6 +196,47 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed 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.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ 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 dade3489c5..6b16ed842c 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 @@ -188,6 +188,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -230,14 +231,59 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - - test.socket.capability:__expect_send({ + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.capability:__expect_send({ mock_device.id, { capability_id = "button", component_id = "main", attribute_id = "button", state = { value = "pushed" } } }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 5 }, { visibility = { displayed = false } }) + ) + ) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + if button_name ~= "button5" then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed"}, { visibility = { displayed = false } }) + ) + ) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.wait_for_events() end @@ -259,4 +305,4 @@ test.register_message_test( } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 032b9c18d1..f28e47dece 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua @@ -129,6 +129,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -150,7 +151,25 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 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 733e79e304..554decefc1 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 @@ -218,6 +218,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -271,6 +272,57 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 4 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) 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 bbac26d61c..67554e92a0 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 @@ -335,6 +335,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -388,6 +389,58 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3" or button_name == "button5" or button_name == "button7") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4" or button_name == "button6" or button_name == "button8") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 8 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua index 46cc01c4f9..0d9f12a697 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua @@ -58,7 +58,7 @@ local function added_handler(self, device) end end device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local remote_control = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua index f01f29715f..9a66a85991 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua @@ -22,6 +22,7 @@ local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" local utils = require 'st.utils' local zdo_messages = require "st.zigbee.zdo" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration @@ -80,8 +81,8 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons})) end device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) -end + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) + end local function zdo_binding_table_handler(driver, device, zb_rx) for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 369b3aaaf9..539b03785b 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -14,6 +14,7 @@ local capabilities = require "st.capabilities" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { { mfr = "CentraLite", model = "3450-L" }, @@ -78,7 +79,7 @@ local function added_handler(self, device) capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) end end - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local zigbee_multi_button = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua index 8775e27bdc..dddef6f595 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua @@ -6,6 +6,7 @@ local Level = zcl_clusters.Level local OnOff = zcl_clusters.OnOff local PowerConfiguration = zcl_clusters.PowerConfiguration local capabilities = require "st.capabilities" +local button_utils = require "button_utils" --[[ The ROBB Wireless Remote Control has 4 or 8 buttons. They are arranged in two columns: @@ -130,7 +131,7 @@ local function added_handler(self, device) device:emit_component_event(comp, capabilities.button.numberOfButtons({ value = number_of_buttons }, { visibility = { displayed = false } })) end - device:emit_event(capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua index 3e02125863..91662454c9 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua @@ -122,11 +122,17 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, 100) + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) + end device:emit_event(capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } })) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({state_change = true})) + if device:get_latest_state("main", capabilities.button.ID, capabilities.button.button.NAME) == nil then + device:emit_event(capabilities.button.button.pushed({state_change = true})) + end end local do_configure = function(self, device) @@ -142,7 +148,6 @@ local is_zigbee_accessory_dimmer = function(opts, driver, device) return true end end - return false end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua index c14e4263c3..134931c2c8 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua @@ -81,8 +81,12 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, DEFAULT_LEVEL) + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) + end end local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) 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 1c1431f518..4e54b67e2f 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -3,6 +3,7 @@ local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local configurations = require "configurations" +local switch_utils = require "switch_utils" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 @@ -89,7 +90,7 @@ local function device_added(driver, device) end device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({ state_change = 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 local function device_init(self, device) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua index d6d094209c..4176e30113 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua @@ -20,6 +20,7 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" local configurations = require "configurations" +local switch_utils = require "switch_utils" local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" @@ -228,11 +229,11 @@ local device_init = function(self, device) end device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) else - device:emit_event(capabilities.colorControl.hue(1)) - device:emit_event(capabilities.colorControl.saturation(1)) - device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) - device:emit_event(capabilities.switchLevel.level(100)) - device:emit_event(capabilities.switch.switch("off")) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.hue.NAME, capabilities.colorControl.hue(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.saturation.NAME, capabilities.colorControl.saturation(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature(6500)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel.NAME, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch("off")) end end diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua new file mode 100644 index 0000000000..7f5429dceb --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils 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 d605f5480d..91683622d1 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 @@ -70,6 +70,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Lifecycle - added test", 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_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, @@ -80,13 +81,22 @@ test.register_coroutine_test( 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.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 } }))) end ) test.register_coroutine_test( "Lifecycle - added test", 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_child.id, "added" }) test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, @@ -94,6 +104,13 @@ 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 }))) + -- 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 } }))) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 153c8325e8..aca9813128 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -15,6 +15,7 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local configurations = require "configurations" +local switch_utils = require "switch_utils" local OnOff = clusters.OnOff local Level = clusters.Level @@ -92,7 +93,7 @@ local function device_init(driver, device) end local function device_added(driver, device) - device:emit_event(capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) end local zigbee_dimming_light = { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 3af5364bbe..6fec190dfa 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -16,6 +16,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Groups = clusters.Groups local Basic = clusters.Basic @@ -41,8 +42,8 @@ local SHADE_STATE_STOP = 2 local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) device:emit_event(hookLockState.hookLockState.unlocked()) device:emit_event(chargingState.chargingState.stopped()) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index 929d5b713d..ae9513734c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -3,6 +3,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering @@ -169,8 +170,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) device:emit_event(deviceInitialization.supportedInitializedState({ "notInitialized", "initializing", "initialized" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(deviceInitialization.initializedState.notInitialized()) device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 6239fb52f0..c7b6b9a50a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -4,6 +4,7 @@ local cluster_base = require "st.zigbee.cluster_base" local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering @@ -93,8 +94,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) 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 37c6b501ed..aa286d24f7 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 @@ -81,6 +81,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -117,6 +118,37 @@ test.register_coroutine_test( cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, data_types.CharString, PREF_SOFT_TOUCH_ON) }) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "stse.deviceInitialization", component_id = "main", + attribute_id = "supportedInitializedState", + state = { value = { "notInitialized", "initializing", "initialized" } }, + visibility = { displayed = false } + } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", deviceInitialization.initializedState.notInitialized()) + ) + 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.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_SOFT_TOUCH_ON) }) end ) 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 f9a9785429..2b095c6c16 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 @@ -77,6 +77,7 @@ end test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -100,6 +101,24 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", hookLockState.hookLockState.unlocked()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", chargingState.chargingState.stopped()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) end ) 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 fd8153ab96..e241a8c81f 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 @@ -67,6 +67,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -85,6 +86,28 @@ test.register_coroutine_test( mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({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.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({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 , diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua new file mode 100644 index 0000000000..2f20ff2b4f --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 window_treatment_utils = {} + +window_treatment_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return window_treatment_utils diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua index 720f88b56f..b2110623f6 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua @@ -18,6 +18,8 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Basic local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +local switch_utils = require "switch_utils" + local EATON_ANYPLACE_SWITCH_FINGERPRINTS = { { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch } @@ -46,7 +48,7 @@ local function basic_get_handler(self, device, cmd) end local function device_added(driver, device) - device:emit_event(capabilities.switch.switch.off()) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch.off()) end local function switch_on_handler(driver, device) diff --git a/drivers/SmartThings/zwave-switch/src/switch_utils.lua b/drivers/SmartThings/zwave-switch/src/switch_utils.lua new file mode 100644 index 0000000000..2d0d2f4d89 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/switch_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils \ No newline at end of file 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 93783aa1a4..1661c34f2c 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 @@ -45,6 +45,7 @@ test.set_test_init_function(test_init) test.register_message_test( "Basic SET 0x00 should be handled as switch off", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -60,6 +61,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.off()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } + }, { channel = "capability", direction = "send", @@ -71,6 +88,7 @@ test.register_message_test( test.register_message_test( "Basic SET 0xFF should be handled as switch on", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -86,6 +104,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } + }, { channel = "capability", direction = "send", diff --git a/drivers/Unofficial/tuya-zigbee/src/button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/init.lua index a3df25a7f8..61599e3d5a 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/init.lua @@ -17,6 +17,7 @@ local clusters = require "st.zigbee.zcl.clusters" local OnOff = clusters.OnOff local device_management = require "st.zigbee.device_management" local PRESENT_ATTRIBUTE_ID = 0x00fd +local tuya_utils = require "tuya_utils" local FINGERPRINTS = { { mfr = "_TZ3000_ja5osu5g", model = "TS004F"}, @@ -36,7 +37,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local tuya_private_cluster_button_handler = function(driver, device, zb_rx) diff --git a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua index f0281f91f5..887826a9f0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua @@ -21,6 +21,7 @@ local data_types = require "st.zigbee.data_types" local messages = require "st.zigbee.messages" local defaults = require "st.zigbee.defaults" local PowerConfiguration = clusters.PowerConfiguration +local tuya_utils = require "tuya_utils" local IASACE = clusters.IASACE @@ -63,7 +64,7 @@ end local function added_handler(driver, device, event, args) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) local magic_spell = {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe} device:send(read_attribute_function(device, clusters.Basic.ID, magic_spell)) diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 1eb2e94340..1d4d494815 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -43,8 +43,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) end local function increase_packet_id(packet_id) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua index a97ff0af10..c3610f55ea 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua @@ -136,6 +136,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should configure all necessary attributes", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -162,6 +163,26 @@ test.register_coroutine_test( mock_device_meian_button.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.zigbee:__expect_send({ mock_device_meian_button.id, tuya_utils.build_tuya_magic_spell_message(mock_device_meian_button) }) + test.socket.zigbee:__expect_send({ + mock_device_meian_button.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) + }) end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua index 431cbc2c48..73567c24e0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua @@ -47,6 +47,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -63,6 +64,20 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", button.button.pushed({ state_change = false })) ) + -- Avoid sending the initial button pushed 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.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) end ) 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 d6beda0de8..44f11a09f4 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -88,6 +88,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) test.socket.capability:__expect_send( mock_simple_device:generate_test_message( @@ -107,6 +108,15 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closed() ) ) + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}}) + ) + ) + end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua index 8e3c707b7d..f28a6a87c1 100644 --- a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua +++ b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua @@ -157,6 +157,12 @@ tuya_utils.build_tuya_magic_spell_message = function(device) }) end +tuya_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + tuya_utils.TUYA_PRIVATE_CLUSTER = TUYA_PRIVATE_CLUSTER tuya_utils.DP_TYPE_BOOL = DP_TYPE_BOOL tuya_utils.DP_TYPE_ENUM = DP_TYPE_ENUM From a3ddbab849615e884f75044e0b7c110b89649d31 Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Tue, 23 Sep 2025 09:49:56 -0500 Subject: [PATCH 159/449] Add reconfigure for zigbee-power-meter driver This adds similar behavior that was previously added for zigbee-switch driver to reconfigure power meter events to be less frequent. This is slightly more complicated as there were a number of subdrivers that had custom configruations. --- .../zigbee-power-meter/src/configurations.lua | 148 ++++++++++++++++++ .../zigbee-power-meter/src/ezex/init.lua | 5 +- .../zigbee-power-meter/src/frient/init.lua | 3 +- .../zigbee-power-meter/src/init.lua | 16 +- .../src/shinasystems/init.lua | 13 +- .../src/test/test_zigbee_power_meter.lua | 141 ++++++++++++++++- ..._zigbee_power_meter_consumption_report.lua | 68 +++++++- ...e_power_meter_consumption_report_sihas.lua | 73 ++++++++- 8 files changed, 450 insertions(+), 17 deletions(-) create mode 100644 drivers/SmartThings/zigbee-power-meter/src/configurations.lua diff --git a/drivers/SmartThings/zigbee-power-meter/src/configurations.lua b/drivers/SmartThings/zigbee-power-meter/src/configurations.lua new file mode 100644 index 0000000000..557e790f76 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/configurations.lua @@ -0,0 +1,148 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local device_def = require "st.device" +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local device_management = require "st.zigbee.device_management" + + +local Status = require "st.zigbee.generated.types.ZclStatus" + +local CONFIGURATION_VERSION_KEY = "_configuration_version" +local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" + + +local configurations = {} + +local active_power_configuration = { + cluster = clusters.ElectricalMeasurement.ID, + attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 5 +} + +local instantaneous_demand_configuration = { + cluster = clusters.SimpleMetering.ID, + attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 5 +} + +configurations.find_cluster_config = function(device, cluster, attribute) + -- This is an internal field, but this is the easiest way to allow the custom configuraitons without + -- larger driver changes + local configured_attrs = device:get_field("__configured_attributes") or {} + for clus, attrs in pairs(configured_attrs) do + if cluster == clus then + for _, attr_config in pairs(attrs) do + if attr_config.attribute == attribute then + local u = require "st.utils" + print(u.stringify_table(attr_config)) + return attr_config + end + end + end + end + return nil +end + + +configurations.check_and_reconfig_devices = function(driver) + for device_id, device in pairs(driver.device_cache) do + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if device:supports_capability(capabilities.powerMeter) then + if device:supports_server_cluster(clusters.ElectricalMeasurement.ID) then + -- Allow for custom configurations as long as the minimum reporting interval is at least 5 + local config = configurations.find_cluster_config(device, clusters.ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ActivePower.ID) + if config == nil or config.minimum_interval < 5 then + config = active_power_configuration + end + device:send(device_management.attr_config(device, config)) + device:add_configured_attribute(config) + end + if device:supports_server_cluster(clusters.SimpleMetering.ID) then + -- Allow for custom configurations as long as the minimum reporting interval is at least 5 + local config = configurations.find_cluster_config(device, clusters.SimpleMetering.ID, SimpleMetering.attributes.InstantaneousDemand.ID) + if config == nil or config.minimum_interval < 5 then + config = instantaneous_demand_configuration + end + device:send(device_management.attr_config(device, config)) + device:add_configured_attribute(config) + + -- perform reconfiguration of summation attribute if it's configured + config = configurations.find_cluster_config(device, clusters.SimpleMetering.ID, SimpleMetering.attributes.CurrentSummationDelivered.ID) + if config ~= nil then + device:send(device_management.attr_config(device, config)) + end + end + end + device:set_field(CONFIGURATION_ATTEMPTED, true, {persist = true}) + end + end + driver._reconfig_timer = nil +end + +configurations.handle_reporting_config_response = function(driver, device, zb_mess) + local dev = device + local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) + if find_child_fn ~= nil then + local child = find_child_fn(device, zb_mess.address_header.src_endpoint.value) + if child ~= nil then + dev = child + end + end + if dev:get_field(CONFIGURATION_ATTEMPTED) == true then + if zb_mess.body.zcl_body.global_status ~= nil and zb_mess.body.zcl_body.global_status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + elseif zb_mess.body.zcl_body.config_records ~= nil then + local config_records = zb_mess.body.zcl_body.config_records + for _, record in ipairs(config_records) do + if zb_mess.address_header.cluster.value == clusters.SimpleMetering.ID then + if record.attr_id.value == clusters.SimpleMetering.attributes.InstantaneousDemand.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + elseif zb_mess.address_header.cluster.value == clusters.ElectricalMeasurement.ID then + if record.attr_id.value == clusters.ElectricalMeasurement.attributes.ActivePower.ID + and record.status.value == Status.SUCCESS then + dev:set_field(CONFIGURATION_VERSION_KEY, driver.current_config_version, {persist = true}) + end + end + end + end + end +end + +configurations.power_reconfig_wrapper = function(orig_function) + local new_init = function(driver, device) + local config_version = device:get_field(CONFIGURATION_VERSION_KEY) + if config_version == nil or config_version < driver.current_config_version then + if driver._reconfig_timer == nil then + driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices") + end + end + orig_function(driver, device) + end + return new_init +end + +return configurations \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua index b0bf581748..ff62547644 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua @@ -17,6 +17,7 @@ local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering local energy_meter_defaults = require "st.zigbee.defaults.energyMeter_defaults" +local configurations = require "configurations" local ZIGBEE_POWER_METER_FINGERPRINTS = { { model = "E240-KR080Z0-HA" } @@ -35,7 +36,7 @@ end local instantaneous_demand_configuration = { cluster = clusters.SimpleMetering.ID, attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 1, + minimum_interval = 5, maximum_interval = 3600, data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, reportable_change = 500 @@ -75,7 +76,7 @@ local ezex_power_meter_handler = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, can_handle = is_ezex_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua index 1c9028f853..9d3f6f83b0 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -13,6 +13,7 @@ -- limitations under the License. local constants = require "st.zigbee.constants" +local configurations = require "configurations" local ZIGBEE_POWER_METER_FINGERPRINTS = { { model = "ZHEMI101" }, @@ -42,7 +43,7 @@ end local frient_power_meter_handler = { NAME = "frient power meter handler", lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, can_handle = is_frient_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index a12272aada..ae98baca8b 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -18,6 +18,9 @@ local defaults = require "st.zigbee.defaults" local zigbee_constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local configurations = require "configurations" local do_configure = function(self, device) device:refresh() @@ -49,13 +52,24 @@ local zigbee_power_meter_driver_template = { capabilities.energyMeter, capabilities.powerConsumptionReport, }, + zigbee_handlers = { + global = { + [SimpleMetering.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurations.handle_reporting_config_response + }, + [ElectricalMeasurement.ID] = { + [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurations.handle_reporting_config_response + } + } + }, + current_config_version = 1, sub_drivers = { require("ezex"), require("frient"), require("shinasystems"), }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, health_check = false, diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua index 3d28f5a0a5..64713547a8 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua @@ -17,6 +17,7 @@ local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement +local configurations = require "configurations" local ZIGBEE_POWER_METER_FINGERPRINTS = { { model = "PMM-300Z1" }, @@ -29,7 +30,7 @@ local POWERMETER_CONFIGURATION_V2 = { cluster = SimpleMetering.ID, attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, minimum_interval = 5, - maximum_interval = 300, + maximum_interval = 450, -- Since the 15 minute report below depends on this report we make this 7.5 minutes data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, reportable_change = 1 }, @@ -37,17 +38,17 @@ local POWERMETER_CONFIGURATION_V2 = { cluster = SimpleMetering.ID, attribute = SimpleMetering.attributes.InstantaneousDemand.ID, minimum_interval = 5, - maximum_interval = 300, + maximum_interval = 3600, data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 1 + reportable_change = 5 }, { -- reporting : no cluster = ElectricalMeasurement.ID, attribute = ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 0, + minimum_interval = 5, maximum_interval = 65535, data_type = ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 1 + reportable_change = 5 } } @@ -124,7 +125,7 @@ local shinasystems_power_meter_handler = { } }, lifecycle_handlers = { - init = device_init, + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, can_handle = is_shinasystems_power_meter 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 710469054f..5f96805360 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 @@ -20,6 +20,12 @@ local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("power-meter.yml") } @@ -27,10 +33,143 @@ 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 + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end test.set_test_init_function(test_init) +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_coroutine_test( + "configuration version below 1", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 config response not success", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.UNSUPPORTED_ATTRIBUTE)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == nil) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records ElectricalMeasurement", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, nil, ElectricalMeasurement.attributes.ActivePower.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + +test.register_coroutine_test( + "configuration version below 1 individual config response records SimpleMetering", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, nil, SimpleMetering.attributes.InstantaneousDemand.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + test.register_message_test( "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", { diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua index 700eab4300..59924fd0ee 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua @@ -20,6 +20,12 @@ local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" local mock_device = test.mock_device.build_test_zigbee_device( { @@ -36,10 +42,41 @@ 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 + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end test.set_test_init_function(test_init) +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + test.register_message_test( "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", { @@ -131,7 +168,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 1, 3600, 500) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 500) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -159,4 +196,31 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "configuration version below 1 use override configs", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 500)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + + 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 536863b409..a4b8b8c037 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 @@ -20,6 +20,13 @@ local SimpleMetering = clusters.SimpleMetering local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local config_reporting_response = require "st.zigbee.zcl.global_commands.configure_reporting_response" +local zb_const = require "st.zigbee.constants" +local zcl_messages = require "st.zigbee.zcl" +local data_types = require "st.zigbee.data_types" +local Status = require "st.zigbee.generated.types.ZclStatus" + local mock_device = test.mock_device.build_test_zigbee_device( { @@ -36,7 +43,38 @@ 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 + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +local function build_config_response_msg(device, cluster, global_status, attribute, attr_status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local config_response_body + if global_status ~= nil then + config_response_body = config_reporting_response.ConfigureReportingResponse({}, global_status) + else + local individual_record = config_reporting_response.ConfigureReportingResponseRecord(attr_status, 0x01, attribute) + config_response_body = config_reporting_response.ConfigureReportingResponse({individual_record}, nil) + end + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(config_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = config_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end test.set_test_init_function(test_init) @@ -137,11 +175,11 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 300, 1) + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, - SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 300, 1) + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 450, 1) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -151,7 +189,7 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 0, 65535, 1) + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 65535, 5) }) test.socket.zigbee:__expect_send({ mock_device.id, @@ -165,4 +203,31 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "configuration version below 1 use override configs", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot") + assert(mock_device:get_field("_configuration_version") == nil) + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 65535, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5)}) + test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 450, 1)}) + test.mock_time.advance_time(5*60 + 1) + test.wait_for_events() + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.SUCCESS)}) + test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, Status.SUCCESS)}) + test.wait_for_events() + assert(mock_device:get_field("_configuration_version") == 1) + end, + { + test_init = function() + -- no op to override auto device add on startup + end + } +) + + test.run_registered_tests() From bcaa37096645bcb1f931cbc67af8ef56d2fa9fc5 Mon Sep 17 00:00:00 2001 From: Alissa Dornbos <79465613+lelandblue@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:45:26 -0400 Subject: [PATCH 160/449] add maileke air quality detector (#1850) (#2103) Co-authored-by: thinkaName <144081204+thinkaName@users.noreply.github.com> Co-authored-by: Steven Green --- .../zigbee-air-quality-detector/config.yml | 6 + .../fingerprints.yml | 6 + .../profiles/air-quality-detector-MultiIR.yml | 38 +++ .../src/MultiIR/custom_clusters.lua | 72 ++++++ .../src/MultiIR/init.lua | 152 ++++++++++++ .../zigbee-air-quality-detector/src/init.lua | 41 ++++ .../test_MultiIR_air_quality_detector.lua | 230 ++++++++++++++++++ tools/localizations/cn.csv | 3 +- 8 files changed, 547 insertions(+), 1 deletion(-) create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/config.yml create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/src/init.lua create mode 100755 drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua diff --git a/drivers/SmartThings/zigbee-air-quality-detector/config.yml b/drivers/SmartThings/zigbee-air-quality-detector/config.yml new file mode 100755 index 0000000000..30e9dceaff --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/config.yml @@ -0,0 +1,6 @@ +name: 'Zigbee Air Quality Detector' +packageKey: 'zigbee-air-quality-detector' +permissions: + zigbee: {} +description: "SmartThings driver for Zigbee air quality detector" +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml new file mode 100755 index 0000000000..b20da987e8 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/fingerprints.yml @@ -0,0 +1,6 @@ +zigbeeManufacturer: + - id: "MultiIR/PMT1006S-SGM-ZTN" + deviceLabel: MultiIR Air Quality Detector + manufacturer: MultiIR + model: PMT1006S-SGM-ZTN + deviceProfileName: air-quality-detector-MultiIR diff --git a/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml new file mode 100755 index 0000000000..7369e1158a --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/profiles/air-quality-detector-MultiIR.yml @@ -0,0 +1,38 @@ +name: air-quality-detector-MultiIR +components: + - id: main + capabilities: + - id: airQualityHealthConcern + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: carbonDioxideMeasurement + version: 1 + - id: carbonDioxideHealthConcern + version: 1 + - id: fineDustSensor + version: 1 + - id: fineDustHealthConcern + version: 1 + - id: veryFineDustSensor + version: 1 + - id: veryFineDustHealthConcern + version: 1 + - id: dustSensor + version: 1 + - id: dustHealthConcern + version: 1 + - id: formaldehydeMeasurement + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: AirQualityDetector diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua new file mode 100755 index 0000000000..8e8f117f49 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/custom_clusters.lua @@ -0,0 +1,72 @@ +-- Copyright 2025 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 data_types = require "st.zigbee.data_types" + +local custom_clusters = { + carbonDioxide = { + id = 0xFCC3, + mfg_specific_code = 0x1235, + attributes = { + measured_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + }, + particulate_matter = { + id = 0xFCC1, + mfg_specific_code = 0x1235, + attributes = { + pm2_5_MeasuredValue = { + id = 0x0000, + value_type = data_types.Uint16, + }, + pm1_0_MeasuredValue = { + id = 0x0001, + value_type = data_types.Uint16, + }, + pm10_MeasuredValue = { + id = 0x0002, + value_type = data_types.Uint16, + } + } + }, + unhealthy_gas = { + id = 0xFCC2, + mfg_specific_code = 0x1235, + attributes = { + CH2O_MeasuredValue = { + id = 0x0000, + value_type = data_types.SinglePrecisionFloat, + }, + tvoc_MeasuredValue = { + id = 0x0001, + value_type = data_types.SinglePrecisionFloat, + } + } + }, + AQI = { + id = 0xFCC5, + mfg_specific_code = 0x1235, + attributes = { + AQI_value = { + id = 0x0000, + value_type = data_types.Uint16, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua new file mode 100755 index 0000000000..3574c06e59 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/MultiIR/init.lua @@ -0,0 +1,152 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local custom_clusters = require "MultiIR/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" + +local RelativeHumidity = clusters.RelativeHumidity +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local MultiIR_SENSOR_FINGERPRINTS = { + { mfr = "MultiIR", model = "PMT1006S-SGM-ZTN" }--This is not a sleep end device +} + +local function can_handle_MultiIR_sensor(opts, driver, device) + for _, fingerprint in ipairs(MultiIR_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function do_refresh(driver, device) + device:send(RelativeHumidity.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue) + send_read_attr_request(device, custom_clusters.particulate_matter, custom_clusters.particulate_matter.attributes.pm10_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue) + send_read_attr_request(device, custom_clusters.unhealthy_gas, custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue) + send_read_attr_request(device, custom_clusters.carbonDioxide, custom_clusters.carbonDioxide.attributes.measured_value) + send_read_attr_request(device, custom_clusters.AQI, custom_clusters.AQI.attributes.AQI_value) +end + +local function airQualityHealthConcern_attr_handler(driver, device, value, zb_rx) + local airQuality_level = "good" + if value.value >= 51 then + airQuality_level = "moderate" + end + if value.value >= 101 then + airQuality_level = "slightlyUnhealthy" + end + if value.value >= 151 then + airQuality_level = "unhealthy" + end + if value.value >= 201 then + airQuality_level = "veryUnhealthy" + end + if value.value >= 301 then + airQuality_level = "hazardous" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.airQualityHealthConcern.airQualityHealthConcern({value = airQuality_level})) +end + +local function carbonDioxide_attr_handler(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideMeasurement.carbonDioxide({value = value.value, unit = "ppm"})) + if value.value <= 1500 then + level = "good" + elseif value.value >= 1501 and value.value <= 2500 then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = level})) +end + +local function particulate_matter_attr_handler(cap,Concern,good,bad) + return function(driver, device, value, zb_rx) + local level = "unhealthy" + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, cap({value = value.value})) + if value.value <= good then + level = "good" + elseif bad > 0 and value.value > good and value.value < bad then + level = "moderate" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, Concern({value = level})) + end +end + +local function CH2O_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.formaldehydeMeasurement.formaldehydeLevel({value = value.value, unit = "mg/m^3"})) +end + +local function tvoc_attr_handler(driver, device, value, zb_rx) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocMeasurement.tvocLevel({value = value.value, unit = "ug/m3"})) + local level = "unhealthy" + if value.value < 600.0 then + level = "good" + end + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.tvocHealthConcern.tvocHealthConcern({value = level})) +end + +local function added_handler(self, device) + do_refresh() +end + +local MultiIR_sensor = { + NAME = "MultiIR air quality detector", + lifecycle_handlers = { + added = added_handler + }, + zigbee_handlers = { + attr = { + [custom_clusters.carbonDioxide.id] = { + [custom_clusters.carbonDioxide.attributes.measured_value.id] = carbonDioxide_attr_handler + }, + [custom_clusters.particulate_matter.id] = { + [custom_clusters.particulate_matter.attributes.pm2_5_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.fineDustSensor.fineDustLevel,capabilities.fineDustHealthConcern.fineDustHealthConcern,75,115),--75 115 is a comparative value of good moderate unhealthy, and 0 is no comparison + [custom_clusters.particulate_matter.attributes.pm1_0_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.veryFineDustSensor.veryFineDustLevel,capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern,100,0), + [custom_clusters.particulate_matter.attributes.pm10_MeasuredValue.id] = particulate_matter_attr_handler(capabilities.dustSensor.dustLevel,capabilities.dustHealthConcern.dustHealthConcern,150,0) + }, + [custom_clusters.unhealthy_gas.id] = { + [custom_clusters.unhealthy_gas.attributes.CH2O_MeasuredValue.id] = CH2O_attr_handler, + [custom_clusters.unhealthy_gas.attributes.tvoc_MeasuredValue.id] = tvoc_attr_handler + }, + [custom_clusters.AQI.id] = { + [custom_clusters.AQI.attributes.AQI_value.id] = airQualityHealthConcern_attr_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = can_handle_MultiIR_sensor +} + +return MultiIR_sensor diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua new file mode 100755 index 0000000000..993ba2a96d --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/init.lua @@ -0,0 +1,41 @@ +-- Copyright 2025 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 ZigbeeDriver = require "st.zigbee" +local capabilities = require "st.capabilities" +local defaults = require "st.zigbee.defaults" + +local zigbee_air_quality_detector_template = { + supported_capabilities = { + capabilities.airQualityHealthConcern, + capabilities.temperatureMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.carbonDioxideMeasurement, + capabilities.carbonDioxideHealthConcern, + capabilities.fineDustSensor, + capabilities.fineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.veryFineDustHealthConcern, + capabilities.dustSensor, + capabilities.dustHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.tvocMeasurement, + capabilities.tvocHealthConcern + }, + sub_drivers = { require("MultiIR") } +} + +defaults.register_for_default_handlers(zigbee_air_quality_detector_template, zigbee_air_quality_detector_template.supported_capabilities) +local zigbee_air_quality_detector_driver = ZigbeeDriver("zigbee-air-quality-detector", zigbee_air_quality_detector_template) +zigbee_air_quality_detector_driver:run() 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 new file mode 100755 index 0000000000..883a3dc5a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua @@ -0,0 +1,230 @@ +-- Copyright 2025 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 cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local SinglePrecisionFloat = require "st.zigbee.data_types.SinglePrecisionFloat" + +local profile_def = t_utils.get_profile_definition("air-quality-detector-MultiIR.yml") +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( +{ + label = "air quality detector", + profile = profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "MultiIR", + model = "PMT1006S-SGM-ZTN", + server_clusters = { 0x0000, 0x0402,0x0405,0xFCC1, 0xFCC2,0xFCC3,0xFCC5} + } + } +}) + +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( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + local read_RelativeHumidity_messge = clusters.RelativeHumidity.attributes.MeasuredValue:read(mock_device) + local read_TemperatureMeasurement_messge = clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + local read_pm2_5_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0000, MFG_CODE) + local read_pm1_0_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0001, MFG_CODE) + local read_pm10_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC1, 0x0002, MFG_CODE) + local read_ch2o_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0000, MFG_CODE) + local read_tvoc_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC2, 0x0001, MFG_CODE) + local read_carbonDioxide_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC3, 0x0000, MFG_CODE) + local read_AQI_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, 0xFCC5, 0x0000, MFG_CODE) + + test.socket.zigbee:__expect_send({mock_device.id, read_RelativeHumidity_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_TemperatureMeasurement_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm2_5_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm1_0_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_pm10_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_ch2o_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_tvoc_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_carbonDioxide_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_AQI_messge}) + end +) + +test.register_message_test( + "Relative humidity reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_device, 40*100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 40 })) + } + } +) + +test.register_message_test( + "Temperature reports should generate correct messages", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C"})) + } + } +) + +test.register_coroutine_test( + "Device reported carbonDioxide and driver emit carbonDioxide and carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 1400 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 1400, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "good"}))) + end +) + +test.register_coroutine_test( + "Device reported pm2.5 and driver emit pm2.5 and fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 74 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 74 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm1.0 and driver emit pm1.0 and veryFineDustHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustSensor.veryFineDustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported pm10 and driver emit pm10 and dustHealthConcern", + function() + local attr_report_data = { + { 0x0002, data_types.Uint16.ID, 69 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustSensor.dustLevel({value = 69 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustHealthConcern.dustHealthConcern.good())) + end +) + +test.register_coroutine_test( + "Device reported ch2o and driver emit ch2o", + function() + local attr_report_data = { + { 0x0000, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.formaldehydeMeasurement.formaldehydeLevel({value = 1000.0, unit = "mg/m^3"}))) + end +) + +test.register_coroutine_test( + "Device reported tvoc and driver emit tvoc", + function() + local attr_report_data = { + { 0x0001, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 9, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({value = 1000.0, unit = "ug/m3"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "Device reported AQI and driver emit airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 50 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "good"}))) + end +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 80067bc5f0..ed47575149 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -99,6 +99,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "ThirdReality Smart Watering Kit",树实智能浇灌套装 "Zemismart ZM24A Smart Curtain", Zemismart ZM24A 智能窗帘 "Greentown Lock(SG20)",绿城门锁(SG20) +"MultiIR Air Quality Detector",MultiIR空气质量检测仪 "Essentials Indoor Lights",基础款室内照明 "Sleepone Ai SX-1",智能床垫 "Smart Mechanical Keyboard MK1",树实智能机械键盘MK1 @@ -114,4 +115,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-B Smart Tubular Motor",威仕达智能管状电机 WSERD50-B "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T -"WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 \ No newline at end of file +"WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 From fa2d948c241a7541e25ec7896bf5691b56457df2 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 28 Aug 2025 09:47:19 -0700 Subject: [PATCH 161/449] Deploy: More granular Jenkins build status If a driver fails to upload, but the bulk upload otherwise succeeds, the Jenkins build should now be marked as unstable and include in the description which driver failed to upload, where, and why. --- Jenkinsfile | 8 ++++++++ tools/deploy.py | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 62bfe12c65..505b8e67f6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,6 +36,7 @@ pipeline { BRANCH = getEnvName() CHANGED_DRIVERS = getChangedDrivers() ENVIRONMENT = "${env.NODE_LABEL.toUpperCase()}" + FAILURE_FILE = "failures.log" } stages { stage('requirements') { @@ -56,6 +57,13 @@ pipeline { stage('environment_update') { steps { sh 'python3 tools/deploy.py' + script { + def failures_file = new File(env.FAILURE_FILE) + if failures_file.exists() { + currentBuild.description += failures_file.getText() + currentBuild.result = 'UNSTABLE' + } + } } } } diff --git a/tools/deploy.py b/tools/deploy.py index e0d3afadbe..2c550149c7 100644 --- a/tools/deploy.py +++ b/tools/deploy.py @@ -26,6 +26,8 @@ VERSION = "version" PACKAGEKEY = "packageKey" +FAILURE_FILE = "failures.log" + BOSE_APPKEY = os.environ.get("BOSE_AUDIONOTIFICATION_APPKEY") SONOS_API_KEY = os.environ.get("SONOS_API_KEY") or "N/A" @@ -170,6 +172,11 @@ if response.status_code == 500 or response.status_code == 429: retries = retries + 1 if retries > 3: + with open("../../"+FAILURE_FILE, 'a') as f: # go up to the root directory to output, since we've changed dirs to the partner directory + f.write("Failed to upload driver to "+ENVIRONMENT+": "+driver) + f.write("Error code: "+str(response.status_code)) + f.write("Error response: "+response.text) + f.write('\n') break # give up if response.status_code == 429: time.sleep(10) From af234ed25b05a7256b96658112f5f47498666210 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 24 Sep 2025 13:51:05 -0700 Subject: [PATCH 162/449] Fixing up extra deploy context --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 505b8e67f6..4c5475e7d3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -59,7 +59,7 @@ pipeline { sh 'python3 tools/deploy.py' script { def failures_file = new File(env.FAILURE_FILE) - if failures_file.exists() { + if (failures_file.exists()) { currentBuild.description += failures_file.getText() currentBuild.result = 'UNSTABLE' } From 71ac87526277981f725dcd9b37bb1172eebb5a5b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 24 Sep 2025 14:24:05 -0700 Subject: [PATCH 163/449] Deploy improvements: use Jenkins file syntax --- Jenkinsfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 4c5475e7d3..425da986db 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -58,9 +58,8 @@ pipeline { steps { sh 'python3 tools/deploy.py' script { - def failures_file = new File(env.FAILURE_FILE) - if (failures_file.exists()) { - currentBuild.description += failures_file.getText() + if (fileExists(env.FAILURE_FILE)) { + currentBuild.description += readFile(env.FAILURE_FILE) currentBuild.result = 'UNSTABLE' } } From 7b3aa03400205e8c826b6f6770e16337abd21e0f Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Thu, 25 Sep 2025 07:41:33 -0500 Subject: [PATCH 164/449] Change ezex power meter to only use SimpleMetering cluster --- .../zigbee-power-meter/src/ezex/init.lua | 15 ++++-- ...t.lua => test_zigbee_power_meter_ezex.lua} | 47 +++---------------- 2 files changed, 18 insertions(+), 44 deletions(-) rename drivers/SmartThings/zigbee-power-meter/src/test/{test_zigbee_power_meter_consumption_report.lua => test_zigbee_power_meter_ezex.lua} (75%) diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua index ff62547644..9e4c6d6eea 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua @@ -16,6 +16,7 @@ local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement local energy_meter_defaults = require "st.zigbee.defaults.energyMeter_defaults" local configurations = require "configurations" @@ -34,11 +35,11 @@ local is_ezex_power_meter = function(opts, driver, device) end local instantaneous_demand_configuration = { - cluster = clusters.SimpleMetering.ID, - attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, minimum_interval = 5, maximum_interval = 3600, - data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, reportable_change = 500 } @@ -50,9 +51,14 @@ end local device_init = function(self, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ActivePower.ID) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ACPowerDivisor.ID) + device:remove_configured_attribute(ElectricalMeasurement.ID, ElectricalMeasurement.attributes.ACPowerMultiplier.ID) device:add_configured_attribute(instantaneous_demand_configuration) end +local function noop_active_power(driver, device, value, zb_rx) end + local function energy_meter_handler(driver, device, value, zb_rx) local raw_value_miliwatts = value.value local raw_value_watts = raw_value_miliwatts / 1000 @@ -72,6 +78,9 @@ local ezex_power_meter_handler = { attr = { [SimpleMetering.ID] = { [SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler + }, + [ElectricalMeasurement.ID] = { + [ElectricalMeasurement.attributes.ActivePower.ID] = noop_active_power } } }, diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua similarity index 75% rename from drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua rename to drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua index 59924fd0ee..7f582d55b7 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua @@ -78,7 +78,7 @@ local function build_config_response_msg(device, cluster, global_status, attribu end test.register_message_test( - "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + "ActivePower Report should not generate an event", { { channel = "zigbee", @@ -90,11 +90,6 @@ test.register_message_test( direction = "receive", message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, 27) }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 2.7, unit = "W" })) } } ) @@ -148,50 +143,20 @@ test.register_coroutine_test( mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered: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, - ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) - }) test.socket.zigbee:__expect_send({ mock_device.id, - ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMetering.ID) }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - SimpleMetering.ID) - }) - test.socket.zigbee:__expect_send({ + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 500) }) - test.socket.zigbee:__expect_send({ + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - ElectricalMeasurement.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) - }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) From 697c1d045d2e4424bc7e18d7599480ad034883de Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:34:07 -0500 Subject: [PATCH 165/449] Gate Pre-58 FW logic for modular profiles (#2350) --- .../src/air-quality-sensor/init.lua | 29 +++++--- .../matter-thermostat/src/init.lua | 67 ++++++++++++------- .../test/test_matter_air_purifier_modular.lua | 18 +++-- .../src/test/test_matter_room_ac_modular.lua | 11 ++- ...st_matter_thermo_multiple_device_types.lua | 10 ++- .../test/test_matter_thermostat_modular.lua | 10 ++- 6 files changed, 81 insertions(+), 64 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua index b4af0445c3..22ce708ea4 100644 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua @@ -401,13 +401,17 @@ local function match_modular_profile(driver, device) device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - 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) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + 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) + + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end end local function do_configure(driver, device) @@ -436,9 +440,14 @@ end local function device_init(driver, device) if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- assume that device is using a modular profile, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) + if version.api >= 15 and version.rpc >= 9 then + -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) + else + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) + end end device:subscribe() end diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index baa20bc31c..1e2c3422f3 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -492,9 +492,14 @@ end local function device_init(driver, device) if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- assume that device is using a modular profile, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) + if version.api >= 15 and version.rpc >= 9 then + -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) + else + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) + end end device:subscribe() device:set_component_to_endpoint_fn(component_to_endpoint) @@ -985,14 +990,18 @@ local function match_modular_profile_air_purifer(driver, device) device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.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) + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.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) - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end end local function match_modular_profile_thermostat(driver, device) @@ -1035,14 +1044,18 @@ local function match_modular_profile_thermostat(driver, device) table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end end local function match_modular_profile_room_ac(driver, device) @@ -1086,15 +1099,19 @@ local function match_modular_profile_room_ac(driver, device) table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.switch.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.switch.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end end local function match_modular_profile(driver, device, device_type) 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 e5f44e3289..4f2b5bd0f4 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 @@ -15,8 +15,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" local uint32 = require "st.matter.data_types.Uint32" @@ -326,10 +324,10 @@ test.register_coroutine_test( test.wait_for_events() - local device_info_copy = utils.deep_copy(mock_device_basic.raw_st_data) - device_info_copy.profile.id = "air-purifier-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_basic:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_basic.id, subscribe_request}) end, { test_init = test_init_basic } @@ -400,10 +398,10 @@ test.register_coroutine_test( test.wait_for_events() - local device_info_copy = utils.deep_copy(mock_device_ap_thermo_aqs.raw_st_data) - device_info_copy.profile.id = "air-purifier-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_ap_thermo_aqs.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_update_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_ap_thermo_aqs:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_ap_thermo_aqs.id, subscribe_request}) end, { test_init = test_init_ap_thermo_aqs_preconfigured } 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 9be85cd84b..2da9e5c023 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 @@ -14,8 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local im = require "st.matter.interaction_model" local uint32 = require "st.matter.data_types.Uint32" @@ -291,11 +289,10 @@ local function test_room_ac_device_type_update_modular_profile(generic_mock_devi clusters.Thermostat.attributes.AttributeList:build_test_report_data(generic_mock_device, 1, {thermostat_attr_list_value}) }) generic_mock_device:expect_metadata_update(expected_metadata) - - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "room-air-conditioner-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("air-purifier-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end 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 2086036be5..7f9577fa8f 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 @@ -15,9 +15,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" -local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" -local utils = require "st.utils" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), @@ -196,10 +194,10 @@ local function test_thermostat_device_type_update_modular_profile(generic_mock_d test.wait_for_events() - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "thermostat-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("thermostat-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end 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 9c42c03ddb..6645d39692 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 @@ -15,10 +15,8 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" -local dkjson = require "dkjson" local im = require "st.matter.interaction_model" local uint32 = require "st.matter.data_types.Uint32" -local utils = require "st.utils" test.disable_startup_messages() @@ -122,10 +120,10 @@ local function test_thermostat_device_type_update_modular_profile(generic_mock_d }) generic_mock_device:expect_metadata_update(expected_metadata) - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "thermostat-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("thermostat-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_basic:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end From 4c273cadbda48e7795b988917050057a71e50370 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:03:22 -0500 Subject: [PATCH 166/449] Matter Switch: Refactor Driver File Organization (#2401) --- .../ElectricalEnergyMeasurement/init.lua | 4 +- .../attributes/CumulativeEnergyImported.lua | 2 +- .../attributes/PeriodicEnergyImported.lua | 2 +- .../server/attributes/init.lua | 2 +- .../types/EnergyMeasurementStruct.lua | 0 .../types/Feature.lua | 0 .../types/init.lua | 2 +- .../ElectricalPowerMeasurement/init.lua | 4 +- .../server/attributes/ActivePower.lua | 0 .../server/attributes/init.lua | 2 +- .../types/Feature.lua | 0 .../ElectricalPowerMeasurement/types/init.lua | 2 +- .../ValveConfigurationAndControl/init.lua | 6 +- .../server/attributes/CurrentLevel.lua | 0 .../server/attributes/CurrentState.lua | 2 +- .../server/attributes/init.lua | 2 +- .../server/commands/Close.lua | 0 .../server/commands/Open.lua | 0 .../server/commands/init.lua | 2 +- .../types/Feature.lua | 0 .../types/ValveStateEnum.lua | 0 .../types/init.lua | 2 +- .../generic_handlers/attribute_handlers.lua | 504 +++++ .../generic_handlers/capability_handlers.lua | 182 ++ .../src/generic_handlers/event_handlers.lua | 93 + .../power_consumption_report.lua | 114 ++ .../SmartThings/matter-switch/src/init.lua | 1661 ++--------------- .../aqara_cube}/init.lua | 0 .../eve_energy}/init.lua | 0 .../third_reality_mk1}/init.lua | 0 .../src/test/test_electrical_sensor.lua | 4 +- .../src/{ => utils}/color_utils.lua | 20 +- .../src/utils/device_configuration.lua | 273 +++ .../embedded_cluster_utils.lua} | 20 +- .../matter-switch/src/utils/switch_fields.lua | 266 +++ .../matter-switch/src/utils/switch_utils.lua | 206 ++ 36 files changed, 1845 insertions(+), 1532 deletions(-) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/init.lua (91%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua (93%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua (93%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/init.lua (85%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/types/Feature.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/types/init.lua (75%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalPowerMeasurement/init.lua (93%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalPowerMeasurement/server/attributes/ActivePower.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalPowerMeasurement/server/attributes/init.lua (85%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalPowerMeasurement/types/Feature.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ElectricalPowerMeasurement/types/init.lua (75%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/init.lua (92%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/attributes/CurrentState.lua (93%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/attributes/init.lua (85%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/commands/Close.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/commands/Open.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/server/commands/init.lua (85%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/types/Feature.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/types/ValveStateEnum.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => embedded_clusters}/ValveConfigurationAndControl/types/init.lua (75%) create mode 100644 drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua rename drivers/SmartThings/matter-switch/src/{aqara-cube => sub_drivers/aqara_cube}/init.lua (100%) rename drivers/SmartThings/matter-switch/src/{eve-energy => sub_drivers/eve_energy}/init.lua (100%) rename drivers/SmartThings/matter-switch/src/{third-reality-mk1 => sub_drivers/third_reality_mk1}/init.lua (100%) rename drivers/SmartThings/matter-switch/src/{ => utils}/color_utils.lua (71%) create mode 100644 drivers/SmartThings/matter-switch/src/utils/device_configuration.lua rename drivers/SmartThings/matter-switch/src/{embedded-cluster-utils.lua => utils/embedded_cluster_utils.lua} (69%) create mode 100644 drivers/SmartThings/matter-switch/src/utils/switch_fields.lua create mode 100644 drivers/SmartThings/matter-switch/src/utils/switch_utils.lua diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua similarity index 91% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index 83bc66aa1b..f2a812055b 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -1,6 +1,6 @@ local cluster_base = require "st.matter.cluster_base" -local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" -local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" local ElectricalEnergyMeasurement = {} ElectricalEnergyMeasurement.ID = 0x0091 diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua index 3dc58635e1..4e35c264f0 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -5,7 +5,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local CumulativeEnergyImported = { ID = 0x0001, NAME = "CumulativeEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function CumulativeEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua index 753b91ea2d..607faa2161 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -5,7 +5,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local PeriodicEnergyImported = { ID = 0x0003, NAME = "PeriodicEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function PeriodicEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua similarity index 85% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua index adfdf42bbf..e137f08918 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -2,7 +2,7 @@ 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("ElectricalEnergyMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalEnergyMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua diff --git a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua similarity index 75% rename from drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua index bb0c39fe0e..5241c3b864 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalEnergyMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -2,7 +2,7 @@ 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("ElectricalEnergyMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalEnergyMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua index 54785d16c6..c9f401dd5c 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -1,6 +1,6 @@ local cluster_base = require "st.matter.cluster_base" -local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" +local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" +local ElectricalPowerMeasurementTypes = require "embedded_clusters.ElectricalPowerMeasurement.types" local ElectricalPowerMeasurement = {} diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua similarity index 85% rename from drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua index 0c30fa8dd4..b1a8d57bde 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -2,7 +2,7 @@ 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("ElectricalPowerMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalPowerMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua diff --git a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua similarity index 75% rename from drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua index 16d13a0688..8016a487c3 100644 --- a/drivers/SmartThings/matter-switch/src/ElectricalPowerMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua @@ -2,7 +2,7 @@ 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("ElectricalPowerMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalPowerMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua similarity index 92% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua index d8ab93412f..0535e3267c 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua @@ -1,7 +1,7 @@ local cluster_base = require "st.matter.cluster_base" -local ValveConfigurationAndControlServerAttributes = require "ValveConfigurationAndControl.server.attributes" -local ValveConfigurationAndControlServerCommands = require "ValveConfigurationAndControl.server.commands" -local ValveConfigurationAndControlTypes = require "ValveConfigurationAndControl.types" +local ValveConfigurationAndControlServerAttributes = require "embedded_clusters.ValveConfigurationAndControl.server.attributes" +local ValveConfigurationAndControlServerCommands = require "embedded_clusters.ValveConfigurationAndControl.server.commands" +local ValveConfigurationAndControlTypes = require "embedded_clusters.ValveConfigurationAndControl.types" local ValveConfigurationAndControl = {} ValveConfigurationAndControl.ID = 0x0081 diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua index 76f156d2a7..514659aa70 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/CurrentState.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua @@ -5,7 +5,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local CurrentState = { ID = 0x0004, NAME = "CurrentState", - base_type = require "ValveConfigurationAndControl.types.ValveStateEnum", + base_type = require "embedded_clusters.ValveConfigurationAndControl.types.ValveStateEnum", } function CurrentState:new_value(...) diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua similarity index 85% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua index 237818d98f..dbffea4e8d 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua @@ -2,7 +2,7 @@ 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("ValveConfigurationAndControl.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ValveConfigurationAndControl.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Close.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/Open.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua similarity index 85% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua index 330e35bba3..74c402c3fd 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/server/commands/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua @@ -2,7 +2,7 @@ local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("ValveConfigurationAndControl.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.ValveConfigurationAndControl.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/Feature.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/ValveStateEnum.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua diff --git a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua similarity index 75% rename from drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua rename to drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua index 835167f485..22aa362e99 100644 --- a/drivers/SmartThings/matter-switch/src/ValveConfigurationAndControl/types/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua @@ -2,7 +2,7 @@ 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("ValveConfigurationAndControl.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ValveConfigurationAndControl.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..3976c2c8a3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -0,0 +1,504 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local version = require "version" +local im = require "st.matter.interaction_model" + +local st_utils = require "st.utils" +local fields = require "utils.switch_fields" +local switch_utils = require "utils.switch_utils" +local color_utils = require "utils.color_utils" + +local power_consumption_reporting = require "generic_handlers.power_consumption_report" + +local AttributeHandlers = {} + +-- [[ ON OFF CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("switch", "switch") + end +end + + +-- [[ LEVEL CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.level_control_current_level_handler(driver, device, ib, response) + if ib.data.value ~= nil then + local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("switchLevel", "level") + end + end +end + +function AttributeHandlers.level_bounds_handler_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local lighting_endpoints = device:get_endpoints(clusters.LevelControl.ID, {feature_bitmap = clusters.LevelControl.FeatureMap.LIGHTING}) + local lighting_support = switch_utils.tbl_contains(lighting_endpoints, ib.endpoint_id) + -- If the lighting feature is supported then we should check if the reported level is at least 1. + if lighting_support and ib.data.value < fields.SWITCH_LEVEL_LIGHTING_MIN then + device.log.warn_with({hub_logs = true}, string.format("Lighting device reported a switch level %d outside of supported capability range", ib.data.value)) + return + end + -- Convert level from given range of 0-254 to range of 0-100. + local level = st_utils.round(ib.data.value / 254.0 * 100) + -- If the device supports the lighting feature, the minimum capability level should be 1 so we do not send a 0 value for the level attribute + if lighting_support and level == 0 then + level = 1 + end + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..minOrMax, ib.endpoint_id, level) + local min = switch_utils.get_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.levelRange({ value = {minimum = min, maximum = max} })) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min level value %d that is not lower than the reported max level value %d", min, max)) + end + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MAX, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.LEVEL_BOUND_RECEIVED..fields.LEVEL_MIN, ib.endpoint_id, nil) + end + end +end + + +-- [[ COLOR CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.current_hue_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == fields.X_Y_COLOR_MODE or ib.data.value == nil then + return + end + local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) +end + +function AttributeHandlers.current_saturation_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == fields.X_Y_COLOR_MODE or ib.data.value == nil then + return + end + local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) +end + +function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, response) + local temp_in_mired = ib.data.value + if temp_in_mired == nil then + return + end + if (temp_in_mired < fields.COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > fields.COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX)) + return + end + local min_temp_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, ib.endpoint_id) + local max_temp_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, ib.endpoint_id) + + local temp = st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_mired) + if min_temp_mired ~= nil and temp_in_mired <= min_temp_mired then + temp = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, ib.endpoint_id) + elseif max_temp_mired ~= nil and temp_in_mired >= max_temp_mired then + temp = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, ib.endpoint_id) + end + + local temp_device = device + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) == true then + temp_device = switch_utils.find_child(device, ib.endpoint_id) or device + end + local most_recent_temp = temp_device:get_field(fields.MOST_RECENT_TEMP) + -- this is to avoid rounding errors from the round-trip conversion of Kelvin to mireds + if most_recent_temp ~= nil and + most_recent_temp <= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and + most_recent_temp >= st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then + temp = most_recent_temp + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperature(temp)) +end + +function AttributeHandlers.current_x_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == fields.HUE_SAT_COLOR_MODE then + return + end + local y = device:get_field(fields.RECEIVED_Y) + --TODO it is likely that both x and y attributes are in the response (not guaranteed though) + -- if they are we can avoid setting fields on the device. + if y == nil then + device:set_field(fields.RECEIVED_X, ib.data.value) + else + local x = ib.data.value + local h, s, _ = color_utils.safe_xy_to_hsv(x, y, nil) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + device:set_field(fields.RECEIVED_Y, nil) + end +end + +function AttributeHandlers.current_y_handler(driver, device, ib, response) + if device:get_field(fields.COLOR_MODE) == fields.HUE_SAT_COLOR_MODE then + return + end + local x = device:get_field(fields.RECEIVED_X) + if x == nil then + device:set_field(fields.RECEIVED_Y, ib.data.value) + else + local y = ib.data.value + local h, s, _ = color_utils.safe_xy_to_hsv(x, y, nil) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + device:set_field(fields.RECEIVED_X, nil) + end +end + +function AttributeHandlers.color_mode_handler(driver, device, ib, response) + if ib.data.value == device:get_field(fields.COLOR_MODE) or (ib.data.value ~= fields.HUE_SAT_COLOR_MODE and ib.data.value ~= fields.X_Y_COLOR_MODE) then + return + end + device:set_field(fields.COLOR_MODE, ib.data.value) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + if ib.data.value == fields.HUE_SAT_COLOR_MODE then + req:merge(clusters.ColorControl.attributes.CurrentHue:read()) + req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) + elseif ib.data.value == fields.X_Y_COLOR_MODE then + req:merge(clusters.ColorControl.attributes.CurrentX:read()) + req:merge(clusters.ColorControl.attributes.CurrentY:read()) + end + if #req.info_blocks > 0 then + device:send(req) + end +end + +--TODO setup configure handler to read this attribute. +function AttributeHandlers.color_capabilities_handler(driver, device, ib, response) + if ib.data.value ~= nil then + if ib.data.value & 0x1 then + device:set_field(fields.HUESAT_SUPPORT, true) + end + end +end + +function AttributeHandlers.color_temp_physical_mireds_bounds_factory(minOrMax) + return function(driver, device, ib, response) + local temp_in_mired = ib.data.value + if temp_in_mired == nil then + return + end + if (temp_in_mired < fields.COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > fields.COLOR_TEMPERATURE_MIRED_MAX) then + device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX)) + return + end + local temp_in_kelvin = switch_utils.mired_to_kelvin(temp_in_mired, minOrMax) + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..minOrMax, ib.endpoint_id, temp_in_kelvin) + -- the minimum color temp in kelvin corresponds to the maximum temp in mireds + if minOrMax == fields.COLOR_TEMP_MIN then + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, ib.endpoint_id, temp_in_mired) + else + switch_utils.set_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, ib.endpoint_id, temp_in_mired) + end + local min = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = max} })) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", min, max)) + end + end + end +end + + +-- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) + local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) +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()) +end + + +-- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.active_power_handler(driver, device, ib, response) + if ib.data.value then + local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end + end +end + + +-- [[ VALVE CONFIGURATION AND CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.valve_configuration_current_state_handler(driver, device, ib, response) + if ib.data.value == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.closed()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.open()) + end +end + +function AttributeHandlers.valve_configuration_current_level_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(ib.data.value)) + end +end + + +-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) + if ib.data.elements.energy then + local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT + device:set_field(fields.TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) + if ib.endpoint_id ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + else + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + end + end +end + +function AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) + if ib.data.elements.energy then + local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT + local latest_energy_report = device:get_field(fields.TOTAL_IMPORTED_ENERGY) or 0 + local summed_energy_report = latest_energy_report + watt_hour_value + device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) + device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) + end +end + +function AttributeHandlers.energy_imported_factory(is_cumulative_report) + return function(driver, device, ib, response) + if not device:get_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED) then + power_consumption_reporting.set_poll_report_timer_and_schedule(device, is_cumulative_report) + end + if is_cumulative_report then + AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) + elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then + AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(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 + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + local profile_name = "" + + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + 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 + profile_name = "button-battery" + break + elseif attr.value == 0x0E then + profile_name = "button-batteryLevel" + break + end + end + if profile_name ~= "" then + if #button_eps > 1 then + profile_name = string.format("%d-", #button_eps) .. profile_name + end + + if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and + device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then + profile_name = profile_name .. "-temperature-humidity" + end + device:try_update_metadata({ profile = profile_name }) + end +end + + +-- [[ SWITCH CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.multi_press_max_handler(driver, device, ib, response) + local max = ib.data.value or 1 --get max number of presses + device.log.debug("Device supports "..max.." presses") + -- capability only supports up to 6 presses + if max > 6 then + device.log.info("Device supports more than 6 presses") + max = 6 + end + local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) + local supportsHeld = switch_utils.tbl_contains(MSL, ib.endpoint_id) + local values = switch_utils.create_multi_press_values_list(max, supportsHeld) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local temp = measured_value / 100.0 + local unit = "C" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) + local min = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id) + local max = switch_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id, nil) + switch_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local humidity = st_utils.round(measured_value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) + end +end + + +-- [[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.fan_mode_handler(driver, device, ib, response) + if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("off")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.LOW then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("low")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.MEDIUM then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("medium")) + elseif ib.data.value == clusters.FanControl.attributes.FanMode.HIGH then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("high")) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("auto")) + end +end + +function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.medium.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.low.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME, + capabilities.fanMode.fanMode.auto.NAME + } + else + supportedFanModes = { + capabilities.fanMode.fanMode.off.NAME, + capabilities.fanMode.fanMode.high.NAME + } + end + local event = capabilities.fanMode.supportedFanModes(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.percent_current_handler(driver, device, ib, response) + if ib.data.value == nil or ib.data.value < 0 or ib.data.value > 100 then + return + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) +end + +return AttributeHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua new file mode 100644 index 0000000000..05a781bc3d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua @@ -0,0 +1,182 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local switch_utils = require "utils.switch_utils" +local fields = require "utils.switch_fields" +local version = require "version" + +local CapabilityHandlers = {} + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +-- [[ SWITCH CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_switch_on(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + --TODO use OnWithRecallGlobalScene for devices with the LT feature + device:send(clusters.OnOff.server.commands.On(device, endpoint_id)) +end + +function CapabilityHandlers.handle_switch_off(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.OnOff.server.commands.Off(device, endpoint_id)) +end + + +-- [[ SWITCH LEVEL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_switch_set_level(driver, device, cmd) + if type(device.register_native_capability_cmd_handler) == "function" then + device:register_native_capability_cmd_handler(cmd.capability, cmd.command) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = math.floor(cmd.args.level/100.0 * 254) + device:send(clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate, 0, 0)) +end + + +-- [[ COLOR CONTROL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_color(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.color.hue) + local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.color.saturation) + req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + else + local x, y, _ = st_utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) + req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + end + device:send(req) +end + +function CapabilityHandlers.handle_set_hue(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.hue) + local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + device:send(req) + else + device.log.warn("Device does not support huesat features on its color control cluster") + end +end + +function CapabilityHandlers.handle_set_saturation(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) + if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then + local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.saturation) + local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + device:send(req) + else + device.log.warn("Device does not support huesat features on its color control cluster") + end +end + + +-- [[ COLOR TEMPERATURE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_color_temperature(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local temp_in_kelvin = cmd.args.temperature + local min_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MIN, endpoint_id) + local max_temp_kelvin = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_KELVIN..fields.COLOR_TEMP_MAX, endpoint_id) + + local temp_in_mired = st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_kelvin) + if min_temp_kelvin ~= nil and temp_in_kelvin <= min_temp_kelvin then + temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) + elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then + temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) + end + local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + device:set_field(fields.MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) + device:send(req) +end + + +-- [[ VALVE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_valve_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ValveConfigurationAndControl.server.commands.Open(device, endpoint_id)) +end + +function CapabilityHandlers.handle_valve_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ValveConfigurationAndControl.server.commands.Close(device, endpoint_id)) +end + + +-- [[ LEVEL CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_level(driver, device, cmd) + local commands = clusters.ValveConfigurationAndControl.server.commands + local endpoint_id = device:component_to_endpoint(cmd.component) + local level = cmd.args.level + if not level then + return + elseif level == 0 then + device:send(commands.Close(device, endpoint_id)) + else + device:send(commands.Open(device, endpoint_id, nil, level)) + end +end + + +-- [[ FAN MODE CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_set_fan_mode(driver, device, cmd) + local fan_mode_id + if cmd.args.fanMode == capabilities.fanMode.fanMode.low.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.medium.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.high.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif cmd.args.fanMode == capabilities.fanMode.fanMode.auto.NAME then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + else + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + end + if fan_mode_id then + local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] + device:send(clusters.FanControl.attributes.FanMode:write(device, fan_ep, fan_mode_id)) + end +end + + +-- [[ FAN SPEED PERCENT CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] + device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) +end + +return CapabilityHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua new file mode 100644 index 0000000000..c16a3bd2d5 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua @@ -0,0 +1,93 @@ +-- Copyright 2025 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 lua_socket = require "socket" +local fields = require "utils.switch_fields" +local switch_utils = require "utils.switch_utils" + +local EventHandlers = {} + + +-- [[ SWITCH CLUSTER EVENTS ]] -- + +function EventHandlers.initial_press_handler(driver, device, ib, response) + if switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event + -- or else we would potentially not create the expected button capability event + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, nil) + elseif switch_utils.get_field_for_endpoint(device, fields.INITIAL_PRESS_ONLY, ib.endpoint_id) then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + elseif switch_utils.get_field_for_endpoint(device, fields.EMULATE_HELD, ib.endpoint_id) then + -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time + switch_utils.set_field_for_endpoint(device, fields.START_BUTTON_PRESS, ib.endpoint_id, lua_socket.gettime(), {persist = false}) + end +end + +-- if the device distinguishes a long press event, it will always be a "held" +-- there's also a "long release" event, but this event is required to come first +function EventHandlers.long_press_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) + if switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, true) + end +end + +function EventHandlers.multi_press_complete_handler(driver, device, ib, response) + -- in the case of multiple button presses + -- emit number of times, multiple presses have been completed + if ib.data and not switch_utils.get_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id) then + local press_value = ib.data.elements.total_number_of_presses_counted.value + --capability only supports up to 6 presses + if press_value < 7 then + local button_event = capabilities.button.button.pushed({state_change = true}) + if press_value == 2 then + button_event = capabilities.button.button.double({state_change = true}) + elseif press_value > 2 then + button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) + end + + device:emit_event_for_endpoint(ib.endpoint_id, button_event) + else + device.log.info(string.format("Number of presses (%d) not supported by capability", press_value)) + end + end + switch_utils.set_field_for_endpoint(device, fields.IGNORE_NEXT_MPC, ib.endpoint_id, nil) +end + +local function emulate_held_event(device, ep) + local now = lua_socket.gettime() + local press_init = switch_utils.get_field_for_endpoint(device, fields.START_BUTTON_PRESS, ep) or now -- if we don't have an init time, assume instant release + if (now - press_init) < fields.TIMEOUT_THRESHOLD then + if (now - press_init) > fields.HELD_THRESHOLD then + device:emit_event_for_endpoint(ep, capabilities.button.button.held({state_change = true})) + else + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = true})) + end + end + switch_utils.set_field_for_endpoint(device, fields.START_BUTTON_PRESS, ep, nil, {persist = false}) +end + +function EventHandlers.short_release_handler(driver, device, ib, response) + if not switch_utils.get_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ib.endpoint_id) then + if switch_utils.get_field_for_endpoint(device, fields.EMULATE_HELD, ib.endpoint_id) then + emulate_held_event(device, ib.endpoint_id) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) + end + end +end + +return EventHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua new file mode 100644 index 0000000000..8692d7b262 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua @@ -0,0 +1,114 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local fields = require "utils.switch_fields" +local embedded_cluster_utils = require "utils.embedded_cluster_utils" +local version = require "version" + +local PowerConsumptionReporting = {} + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +-- [[ POWER CONSUMPTION REPORT HELPER FUNCTIONS ]] -- + +-- Return an ISO-8061 timestamp in UTC +local function iso8061Timestamp(time) + return os.date("!%Y-%m-%dT%H:%M:%SZ", time) +end + +-- Emit the capability event capturing the latest energy delta and timestamps +local function send_import_poll_report(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + else + device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ + start = iso8061Timestamp(last_time), + ["end"] = iso8061Timestamp(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + end +end + +-- Set the poll report schedule on the timer defined by IMPORT_REPORT_TIMEOUT +local function create_poll_report_schedule(device) + local import_timer = device.thread:call_on_schedule( + device:get_field(fields.IMPORT_REPORT_TIMEOUT), function() + send_import_poll_report(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) + end, "polling_import_report_schedule_timer" + ) + device:set_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) +end + +function PowerConsumptionReporting.set_poll_report_timer_and_schedule(device, is_cumulative_report) + local cumul_eps = embedded_cluster_utils.get_endpoints(device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) + if #cumul_eps == 0 then + device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = true}) + end + if #cumul_eps > 0 and not is_cumulative_report then + return + elseif not device:get_field(fields.SUBSCRIPTION_REPORT_OCCURRED) then + device:set_field(fields.SUBSCRIPTION_REPORT_OCCURRED, true) + elseif not device:get_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP) then + device:set_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP, os.time()) + else + local first_timestamp = device:get_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP) + local second_timestamp = os.time() + local report_interval_secs = second_timestamp - first_timestamp + device:set_field(fields.IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL)) + -- the poll schedule is only needed for devices that support powerConsumption + -- and enable powerConsumption when energy management is defined in root endpoint(0). + if device:supports_capability(capabilities.powerConsumptionReport) or + device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then + create_poll_report_schedule(device) + end + device:set_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) + end +end + +function PowerConsumptionReporting.delete_import_poll_schedule(device) + local import_poll_timer = device:get_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER) + if import_poll_timer then + device.thread:cancel_timer(import_poll_timer) + device:set_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER, nil) + device:set_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) + end +end + +return PowerConsumptionReporting diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index a4bf6e64f9..6deb8ab603 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -12,704 +12,97 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local im = require "st.matter.interaction_model" local MatterDriver = require "st.matter.driver" -local lua_socket = require "socket" -local utils = require "st.utils" +local capabilities = require "st.capabilities" local device_lib = require "st.device" -local embedded_cluster_utils = require "embedded-cluster-utils" +local clusters = require "st.matter.clusters" +local log = require "log" local version = require "version" --- Include driver-side definitions when lua libs api version is < 11 -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" -end - -local MOST_RECENT_TEMP = "mostRecentTemp" -local RECEIVED_X = "receivedX" -local RECEIVED_Y = "receivedY" -local HUESAT_SUPPORT = "huesatSupport" -local MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 --- These values are a "sanity check" to check that values we are getting are reasonable -local COLOR_TEMPERATURE_KELVIN_MAX = 15000 -local COLOR_TEMPERATURE_KELVIN_MIN = 1000 -local COLOR_TEMPERATURE_MIRED_MAX = MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN -local COLOR_TEMPERATURE_MIRED_MIN = MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX -local SWITCH_LEVEL_LIGHTING_MIN = 1 -local CURRENT_HUESAT_ATTR_MIN = 0 -local CURRENT_HUESAT_ATTR_MAX = 254 - --- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for --- devices that were joined to this driver as MCD devices before the transition --- to join switch devices as parent-child. This value will exist in the device --- table for devices that joined prior to this transition, and is also used for --- button devices that require component mapping. -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" -local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" -local COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" -local COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" -local COLOR_TEMP_MIN = "__color_temp_min" -local COLOR_TEMP_MAX = "__color_temp_max" -local LEVEL_BOUND_RECEIVED = "__level_bound_received" -local LEVEL_MIN = "__level_min" -local LEVEL_MAX = "__level_max" -local COLOR_MODE = "__color_mode" - -local updated_fields = { - { current_field_name = "__component_to_endpoint_map_button", updated_field_name = COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } -} - -local HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION -local X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - -local AGGREGATOR_DEVICE_TYPE_ID = 0x000E -local ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 -local DIMMABLE_LIGHT_DEVICE_TYPE_ID = 0x0101 -local COLOR_TEMP_LIGHT_DEVICE_TYPE_ID = 0x010C -local EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID = 0x010D -local ON_OFF_PLUG_DEVICE_TYPE_ID = 0x010A -local DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B -local ON_OFF_SWITCH_ID = 0x0103 -local ON_OFF_DIMMER_SWITCH_ID = 0x0104 -local ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 -local MOUNTED_ON_OFF_CONTROL_ID = 0x010F -local MOUNTED_DIMMABLE_LOAD_CONTROL_ID = 0x0110 -local GENERIC_SWITCH_ID = 0x000F -local ELECTRICAL_SENSOR_ID = 0x0510 -local device_type_profile_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", - [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", - [COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = "light-level-colorTemperature", - [EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = "light-color-level", - [ON_OFF_PLUG_DEVICE_TYPE_ID] = "plug-binary", - [DIMMABLE_PLUG_DEVICE_TYPE_ID] = "plug-level", - [ON_OFF_SWITCH_ID] = "switch-binary", - [ON_OFF_DIMMER_SWITCH_ID] = "switch-level", - [ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", - [MOUNTED_ON_OFF_CONTROL_ID] = "switch-binary", - [MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", -} +local fields = require "utils.switch_fields" +local switch_utils = require "utils.switch_utils" +local cfg = require "utils.device_configuration" +local device_cfg = cfg.DeviceCfg +local switch_cfg = cfg.SwitchCfg +local button_cfg = cfg.ButtonCfg -local device_type_attribute_map = { - [ON_OFF_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff - }, - [DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds - }, - [EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY - }, - [ON_OFF_PLUG_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff - }, - [DIMMABLE_PLUG_DEVICE_TYPE_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [ON_OFF_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff - }, - [ON_OFF_DIMMER_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [ON_OFF_COLOR_DIMMER_SWITCH_ID] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY - }, - [GENERIC_SWITCH_ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete - }, - [ELECTRICAL_SENSOR_ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported - } -} +local attribute_handlers = require "generic_handlers.attribute_handlers" +local event_handlers = require "generic_handlers.event_handlers" +local capability_handlers = require "generic_handlers.capability_handlers" +local power_consumption_reporting = require "generic_handlers.power_consumption_report" -local child_device_profile_overrides_per_vendor_id = { - [0x1321] = { - { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, - { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, - }, - [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) - } -} - -local detect_matter_thing - -local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local FIRST_IMPORT_REPORT_TIMESTAMP = "__first_import_report_timestamp" -local IMPORT_POLL_TIMER_SETTING_ATTEMPTED = "__import_poll_timer_setting_attempted" -local IMPORT_REPORT_TIMEOUT = "__import_report_timeout" -local TOTAL_IMPORTED_ENERGY = "__total_imported_energy" -local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" -local RECURRING_IMPORT_REPORT_POLL_TIMER = "__recurring_import_report_poll_timer" -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds -local SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" -local CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - --- Return an ISO-8061 timestamp in UTC -local function iso8061Timestamp(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local function delete_import_poll_schedule(device) - local import_poll_timer = device:get_field(RECURRING_IMPORT_REPORT_POLL_TIMER) - if import_poll_timer then - device.thread:cancel_timer(import_poll_timer) - device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, nil) - device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) - end -end - -local function send_import_poll_report(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - device:set_field(LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - end -end - -local function create_poll_report_schedule(device) - local import_timer = device.thread:call_on_schedule( - device:get_field(IMPORT_REPORT_TIMEOUT), function() - send_import_poll_report(device, device:get_field(TOTAL_IMPORTED_ENERGY)) - end, "polling_import_report_schedule_timer" - ) - device:set_field(RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) -end - -local function set_poll_report_timer_and_schedule(device, is_cumulative_report) - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - if #cumul_eps == 0 then - device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = true}) - end - if #cumul_eps > 0 and not is_cumulative_report then - return - elseif not device:get_field(SUBSCRIPTION_REPORT_OCCURRED) then - device:set_field(SUBSCRIPTION_REPORT_OCCURRED, true) - elseif not device:get_field(FIRST_IMPORT_REPORT_TIMESTAMP) then - device:set_field(FIRST_IMPORT_REPORT_TIMESTAMP, os.time()) - else - local first_timestamp = device:get_field(FIRST_IMPORT_REPORT_TIMESTAMP) - local second_timestamp = os.time() - local report_interval_secs = second_timestamp - first_timestamp - device:set_field(IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumption - -- and enable powerConsumption when energy management is defined in root endpoint(0). - if device:supports_capability(capabilities.powerConsumptionReport) or - device:get_field(ENERGY_MANAGEMENT_ENDPOINT) then - create_poll_report_schedule(device) - end - device:set_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) - end -end - -local START_BUTTON_PRESS = "__start_button_press" -local TIMEOUT_THRESHOLD = 10 --arbitrary timeout -local HELD_THRESHOLD = 1 --- this is the number of buttons for which we have a static profile already made -local STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8, 9} - --- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a --- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because --- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used --- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. -local IGNORE_NEXT_MPC = "__ignore_next_mpc" - --- These are essentially storing the supported features of a given endpoint --- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint -local EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side -local SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete -local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) - -local TEMP_BOUND_RECEIVED = "__temp_bound_received" -local TEMP_MIN = "__temp_min" -local TEMP_MAX = "__temp_max" - -local AQARA_MANUFACTURER_ID = 0x115F -local AQARA_CLIMATE_SENSOR_W100_ID = 0x2004 - ---helper function to create list of multi press values -local function create_multi_press_values_list(size, supportsHeld) - local list = {"pushed", "double"} - if supportsHeld then table.insert(list, "held") end - -- add multi press values of 3 or greater to the list - for i=3, size do - table.insert(list, string.format("pushed_%dx", i)) - end - return list -end - -local function tbl_contains(array, value) - for _, element in ipairs(array) do - if element == value then - return true - end - end - return false -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function init_press(device, endpoint) - set_field_for_endpoint(device, START_BUTTON_PRESS, endpoint, lua_socket.gettime(), {persist = false}) -end - -local function emulate_held_event(device, ep) - local now = lua_socket.gettime() - local press_init = get_field_for_endpoint(device, START_BUTTON_PRESS, ep) or now -- if we don't have an init time, assume instant release - if (now - press_init) < TIMEOUT_THRESHOLD then - if (now - press_init) > HELD_THRESHOLD then - device:emit_event_for_endpoint(ep, capabilities.button.button.held({state_change = true})) - else - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = true})) - end - end - set_field_for_endpoint(device, START_BUTTON_PRESS, ep, nil, {persist = false}) -end - -local function convert_huesat_st_to_matter(val) - return utils.clamp_value(math.floor((val * 0xFE) / 100.0 + 0.5), CURRENT_HUESAT_ATTR_MIN, CURRENT_HUESAT_ATTR_MAX) -end - -local function mired_to_kelvin(value, minOrMax) - if value == 0 then -- shouldn't happen, but has - value = 1 - log.warn(string.format("Received a color temperature of 0 mireds. Using a color temperature of 1 mired to avoid divide by zero")) - end - -- We divide inside the rounding and multiply outside of it because we expect these - -- bounds to be multiples of 100. For the maximum mired value (minimum K value), - -- add 1 before converting and round up to nearest hundreds. For the minimum mired - -- (maximum K value) value, subtract 1 before converting and round down to nearest - -- hundreds. Note that 1 is added/subtracted from the mired value in order to avoid - -- rounding errors from the conversion of Kelvin to mireds. - local kelvin_step_size = 100 - local rounding_value = 0.5 - if minOrMax == COLOR_TEMP_MIN then - return utils.round(MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value + 1)) + rounding_value) * kelvin_step_size - elseif minOrMax == COLOR_TEMP_MAX then - return utils.round(MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value - 1)) - rounding_value) * kelvin_step_size - else - log.warn_with({hub_logs = true}, "Attempted to convert temperature unit for an undefined value") - end -end - ---- device_type_supports_button_switch_combination helper function used to check ---- whether the device type for an endpoint is currently supported by a profile for ---- combination button/switch devices. -local function device_type_supports_button_switch_combination(device, endpoint_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == endpoint_id then - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(child_device_profile_overrides_per_vendor_id[0x115F]) do - if device.manufacturer_info.product_id == fingerprint.product_id then - return false -- For Aqara Dimmer Switch with Button. - end - end - return true - end - end - end - end - return false -end - -local function get_first_non_zero_endpoint(endpoints) - table.sort(endpoints) - for _,ep in ipairs(endpoints) do - if ep ~= 0 then -- 0 is the matter RootNode endpoint - return ep - end - end - return nil -end - ---- find_default_endpoint is a helper function to handle situations where ---- device does not have endpoint ids in sequential order from 1 -local function find_default_endpoint(device) - if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == AQARA_CLIMATE_SENSOR_W100_ID then - -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 - return device.MATTER_DEFAULT_ENDPOINT - end - - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - - -- Return the first switch endpoint as the default endpoint if no button endpoints are present - if #button_eps == 0 and #switch_eps > 0 then - return get_first_non_zero_endpoint(switch_eps) - end - - -- Return the first button endpoint as the default endpoint if no switch endpoints are present - if #switch_eps == 0 and #button_eps > 0 then - return get_first_non_zero_endpoint(button_eps) - end - - -- If both switch and button endpoints are present, check the device type on the main switch - -- endpoint. If it is not a supported device type, return the first button endpoint as the - -- default endpoint. - if #switch_eps > 0 and #button_eps > 0 then - local main_endpoint = get_first_non_zero_endpoint(switch_eps) - if device_type_supports_button_switch_combination(device, main_endpoint) then - return main_endpoint - else - device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") - return get_first_non_zero_endpoint(button_eps) - end - end - - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return device.MATTER_DEFAULT_ENDPOINT -end - -local function component_to_endpoint(device, component) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - if map[component] then - return map[component] - end - return find_default_endpoint(device) -end - -local function endpoint_to_component(device, ep) - local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then - return component - end - end - return "main" -end - -local function check_field_name_updates(device) - for _, field in ipairs(updated_fields) do - if device:get_field(field.current_field_name) then - if field.updated_field_name ~= nil then - device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) - end - device:set_field(field.current_field_name, nil) - end - end -end - -local function assign_child_profile(device, child_ep) - local profile - - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = device_type_profile_map[id] - break - end - end - - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end - end - - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end -local function configure_buttons(device) - local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - 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}) - local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) - - for _, ep in ipairs(ms_eps) do - if device.profile.components[endpoint_to_component(device, ep)] then - device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) - local supportedButtonValues_event - -- this ordering is important, since MSM & MSL devices must also support MSR - if tbl_contains(msm_eps, ep) then - supportedButtonValues_event = nil -- deferred to the max press handler - device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) - set_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) - elseif tbl_contains(msl_eps, ep) then - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) - elseif tbl_contains(msr_eps, ep) then - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) - set_field_for_endpoint(device, EMULATE_HELD, ep, true, {persist = true}) - else -- this switch endpoint only supports momentary switch, no release events - supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) - set_field_for_endpoint(device, INITIAL_PRESS_ONLY, ep, true, {persist = true}) - end +local SwitchLifecycleHandlers = {} - if supportedButtonValues_event then - device:emit_event_for_endpoint(ep, supportedButtonValues_event) - end - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) - else - device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) - end +function SwitchLifecycleHandlers.device_added(driver, device) + -- refresh child devices to get an initial attribute state for OnOff in case child device + -- was created after the initial subscription report + if device.network_type == device_lib.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.attributes.OnOff:read(device)) end -end - -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) -end -local function build_button_component_map(device, main_endpoint, button_eps) - -- create component mapping on the main profile button endpoints - table.sort(button_eps) - local component_map = {} - component_map["main"] = main_endpoint - for component_num, ep in ipairs(button_eps) do - if ep ~= main_endpoint then - local button_component = "button" - if #button_eps > 1 then - button_component = button_component .. component_num - end - component_map[button_component] = ep - end - end - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) + -- call device init in case init is not called after added due to device caching + SwitchLifecycleHandlers.device_init(driver, device) end -local function build_button_profile(device, main_endpoint, num_button_eps) - local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep - if device_type_supports_button_switch_combination(device, main_endpoint) then - profile_name = "light-level-" .. profile_name - end - local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 - if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler - device:send(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:try_update_metadata({profile = profile_name}) +function SwitchLifecycleHandlers.do_configure(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + device_cfg.match_profile(driver, device) end end -local function build_child_switch_profiles(driver, device, main_endpoint) - local num_switch_server_eps = 0 - local parent_child_device = false - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - for idx, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end - end - end - - -- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches - -- is only run once, but find_child function should be set on each driver init. - if parent_child_device then - device:set_field(IS_PARENT_CHILD_DEVICE, true, {persist = true}) +function SwitchLifecycleHandlers.driver_switched(driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + device_cfg.match_profile(driver, device) end - - -- this is needed in initialize_buttons_and_switches - return num_switch_server_eps end -local function handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - local cluster_id = 0 - for _, ep in ipairs(device.endpoints) do - -- main_endpoint only supports server cluster by definition of get_endpoints() - if main_endpoint == ep.endpoint_id then - for _, dt in ipairs(ep.device_types) do - -- no device type that is not in the switch subset should be considered. - if (ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= ON_OFF_COLOR_DIMMER_SWITCH_ID) then - cluster_id = math.max(cluster_id, dt.device_type_id) - end - end - break +function SwitchLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then + button_cfg.configure_buttons(device) end end - - if device_type_profile_map[cluster_id] then - device:try_update_metadata({profile = device_type_profile_map[cluster_id]}) - end end -local function initialize_buttons_and_switches(driver, device, main_endpoint) - local profile_found = false - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if tbl_contains(STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - build_button_profile(device, main_endpoint, #button_eps) - -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. - -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field - build_button_component_map(device, main_endpoint, button_eps) - configure_buttons(device) - profile_found = true - end - - -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled - -- while building switch child profiles - local num_switch_server_eps = build_child_switch_profiles(driver, device, main_endpoint) - - -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. - -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. - -- Note: since their device type isn't supported, these devices join as a matter-thing. - if num_switch_server_eps > 0 and detect_matter_thing(device) then - handle_light_switch_with_onOff_server_clusters(device, main_endpoint) - profile_found = true - end - return profile_found +function SwitchLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") + power_consumption_reporting.delete_import_poll_schedule(device) end -local function detect_bridge(device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AGGREGATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false -end - -local function device_init(driver, device) +function SwitchLifecycleHandlers.device_init(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER then - check_field_name_updates(device) - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) + switch_utils.check_field_name_updates(device) + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + device:set_find_child(switch_utils.find_child) end - local main_endpoint = find_default_endpoint(device) + local main_endpoint = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices for idx, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= main_endpoint then if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = assign_child_profile(device, ep) + local child_profile = switch_cfg.assign_child_profile(device, ep) if idx == 1 and string.find(child_profile, "energy") then -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) end end local id = 0 for _, dt in ipairs(ep.device_types) do id = math.max(id, dt.device_type_id) end - for _, attr in pairs(device_type_attribute_map[id] or {}) do - if id == GENERIC_SWITCH_ID and + for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do + if id == fields.GENERIC_SWITCH_ID and attr ~= clusters.PowerSource.attributes.BatPercentRemaining and attr ~= clusters.PowerSource.attributes.BatChargeLevel then device:add_subscribed_event(attr) @@ -719,816 +112,96 @@ local function device_init(driver, device) end end end - device:subscribe() - end -end - -local function match_profile(driver, device) - local main_endpoint = find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local profile_found = initialize_buttons_and_switches(driver, device, main_endpoint) - if device:get_field(IS_PARENT_CHILD_DEVICE) then - device:set_find_child(find_child) - end - if profile_found then - return - end - - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) - local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) - local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then - profile_name = "water-valve" - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, - {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - profile_name = profile_name .. "-level" - end - elseif #fan_eps > 0 then - profile_name = "light-color-level-fan" - end - if profile_name then - device:try_update_metadata({ profile = profile_name }) - end -end - -local function do_configure(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not detect_bridge(device) then - match_profile(driver, device) - end -end - -local function driver_switched(driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and not detect_bridge(device) then - match_profile(driver, device) - end -end - -local function device_removed(driver, device) - log.info("device removed") - delete_import_poll_schedule(device) -end - -local function handle_switch_on(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - --TODO use OnWithRecallGlobalScene for devices with the LT feature - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function handle_set_switch_level(driver, device, cmd) - if type(device.register_native_capability_cmd_handler) == "function" then - device:register_native_capability_cmd_handler(cmd.capability, cmd.command) - end - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.level/100.0 * 254) - local req = clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate, 0, 0) - device:send(req) -end - -local TRANSITION_TIME = 0 --1/10ths of a second --- When sent with a command, these options mask and override bitmaps cause the command --- to take effect when the switch/light is off. -local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 - -local function handle_set_color(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local hue = convert_huesat_st_to_matter(cmd.args.color.hue) - local sat = convert_huesat_st_to_matter(cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - else - local x, y, _ = utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - end - device:send(req) -end - -local function handle_set_hue(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local hue = convert_huesat_st_to_matter(cmd.args.hue) - local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:send(req) - else - log.warn("Device does not support huesat features on its color control cluster") - end -end - -local function handle_set_saturation(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) - if tbl_contains(huesat_endpoints, endpoint_id) then - local sat = convert_huesat_st_to_matter(cmd.args.saturation) - local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:send(req) - else - log.warn("Device does not support huesat features on its color control cluster") - end -end - -local function handle_set_color_temperature(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local temp_in_kelvin = cmd.args.temperature - local min_temp_kelvin = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, endpoint_id) - local max_temp_kelvin = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, endpoint_id) - - local temp_in_mired = utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_kelvin) - if min_temp_kelvin ~= nil and temp_in_kelvin <= min_temp_kelvin then - temp_in_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, endpoint_id) - elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then - temp_in_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, endpoint_id) - end - local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - device:set_field(MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) - device:send(req) -end - -local function handle_valve_open(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.ValveConfigurationAndControl.server.commands.Open(device, endpoint_id) - device:send(req) -end - -local function handle_valve_close(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.ValveConfigurationAndControl.server.commands.Close(device, endpoint_id) - device:send(req) -end - -local function handle_set_level(driver, device, cmd) - local commands = clusters.ValveConfigurationAndControl.server.commands - local endpoint_id = device:component_to_endpoint(cmd.component) - local level = cmd.args.level - if not level then - return - elseif level == 0 then - device:send(commands.Close(device, endpoint_id)) - else - device:send(commands.Open(device, endpoint_id, nil, level)) - end -end - -local function set_fan_mode(driver, device, cmd) - local fan_mode_id - if cmd.args.fanMode == capabilities.fanMode.fanMode.low.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.LOW - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.medium.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.high.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH - elseif cmd.args.fanMode == capabilities.fanMode.fanMode.auto.NAME then - fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO - else - fan_mode_id = clusters.FanControl.attributes.FanMode.OFF - end - if fan_mode_id then - local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] - device:send(clusters.FanControl.attributes.FanMode:write(device, fan_ep, fan_mode_id)) - end -end - -local function set_fan_speed_percent(driver, device, cmd) - local speed = math.floor(cmd.args.percent) - local fan_ep = device:get_endpoints(clusters.FanControl.ID)[1] - device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) -end - --- Fallback handler for responses that dont have their own handler -local function matter_handler(driver, device, response_block) - log.info(string.format("Fallback handler for %s", response_block)) -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.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("switch", "switch") - end -end - -local function level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("switchLevel", "level") - end - end -end - -local function hue_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == X_Y_COLOR_MODE or ib.data.value == nil then - return - end - local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) -end -local function sat_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == X_Y_COLOR_MODE or ib.data.value == nil then - return - end - local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) -end - -local function temp_attr_handler(driver, device, ib, response) - local temp_in_mired = ib.data.value - if temp_in_mired == nil then - return - end - if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then - device.log.warn_with({hub_logs = true}, string.format("Device reported color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) - return - end - local min_temp_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, ib.endpoint_id) - local max_temp_mired = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, ib.endpoint_id) - - local temp = utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/temp_in_mired) - if min_temp_mired ~= nil and temp_in_mired <= min_temp_mired then - temp = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, ib.endpoint_id) - elseif max_temp_mired ~= nil and temp_in_mired >= max_temp_mired then - temp = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, ib.endpoint_id) - end - - local temp_device = device - if device:get_field(IS_PARENT_CHILD_DEVICE) == true then - temp_device = find_child(device, ib.endpoint_id) or device - end - local most_recent_temp = temp_device:get_field(MOST_RECENT_TEMP) - -- this is to avoid rounding errors from the round-trip conversion of Kelvin to mireds - if most_recent_temp ~= nil and - most_recent_temp <= utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired - 1)) and - most_recent_temp >= utils.round(MIRED_KELVIN_CONVERSION_CONSTANT/(temp_in_mired + 1)) then - temp = most_recent_temp - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperature(temp)) -end - -local mired_bounds_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - local temp_in_mired = ib.data.value - if temp_in_mired == nil then - return - end - if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then - device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX)) - return - end - local temp_in_kelvin = mired_to_kelvin(temp_in_mired, minOrMax) - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..minOrMax, ib.endpoint_id, temp_in_kelvin) - -- the minimum color temp in kelvin corresponds to the maximum temp in mireds - if minOrMax == COLOR_TEMP_MIN then - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MAX, ib.endpoint_id, temp_in_mired) - else - set_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_MIRED..COLOR_TEMP_MIN, ib.endpoint_id, temp_in_mired) - end - local min = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, COLOR_TEMP_BOUND_RECEIVED_KELVIN..COLOR_TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = max} })) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", min, max)) - end - end - end -end - -local level_bounds_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local lighting_endpoints = device:get_endpoints(clusters.LevelControl.ID, {feature_bitmap = clusters.LevelControl.FeatureMap.LIGHTING}) - local lighting_support = tbl_contains(lighting_endpoints, ib.endpoint_id) - -- If the lighting feature is supported then we should check if the reported level is at least 1. - if lighting_support and ib.data.value < SWITCH_LEVEL_LIGHTING_MIN then - device.log.warn_with({hub_logs = true}, string.format("Lighting device reported a switch level %d outside of supported capability range", ib.data.value)) - return - end - -- Convert level from given range of 0-254 to range of 0-100. - local level = utils.round(ib.data.value / 254.0 * 100) - -- If the device supports the lighting feature, the minimum capability level should be 1 so we do not send a 0 value for the level attribute - if lighting_support and level == 0 then - level = 1 - end - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..minOrMax, ib.endpoint_id, level) - local min = get_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.levelRange({ value = {minimum = min, maximum = max} })) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min level value %d that is not lower than the reported max level value %d", min, max)) - end - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MAX, ib.endpoint_id, nil) - set_field_for_endpoint(device, LEVEL_BOUND_RECEIVED..LEVEL_MIN, ib.endpoint_id, nil) - end - end -end - -local color_utils = require "color_utils" - -local function x_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == HUE_SAT_COLOR_MODE then - return - end - local y = device:get_field(RECEIVED_Y) - --TODO it is likely that both x and y attributes are in the response (not guaranteed though) - -- if they are we can avoid setting fields on the device. - if y == nil then - device:set_field(RECEIVED_X, ib.data.value) - else - local x = ib.data.value - local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) - device:set_field(RECEIVED_Y, nil) - end -end - -local function y_attr_handler(driver, device, ib, response) - if device:get_field(COLOR_MODE) == HUE_SAT_COLOR_MODE then - return - end - local x = device:get_field(RECEIVED_X) - if x == nil then - device:set_field(RECEIVED_Y, ib.data.value) - else - local y = ib.data.value - local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) - device:set_field(RECEIVED_X, nil) - end -end - -local function color_mode_attr_handler(driver, device, ib, response) - if ib.data.value == device:get_field(COLOR_MODE) or (ib.data.value ~= HUE_SAT_COLOR_MODE and ib.data.value ~= X_Y_COLOR_MODE) then - return - end - device:set_field(COLOR_MODE, ib.data.value) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - if ib.data.value == HUE_SAT_COLOR_MODE then - req:merge(clusters.ColorControl.attributes.CurrentHue:read()) - req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) - elseif ib.data.value == X_Y_COLOR_MODE then - req:merge(clusters.ColorControl.attributes.CurrentX:read()) - req:merge(clusters.ColorControl.attributes.CurrentY:read()) - end - if #req.info_blocks > 0 then - device:send(req) - end -end - ---TODO setup configure handler to read this attribute. -local function color_cap_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - if ib.data.value & 0x1 then - device:set_field(HUESAT_SUPPORT, true) - end - end -end - -local function illuminance_attr_handler(driver, device, ib, response) - local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) -end - -local function occupancy_attr_handler(driver, device, ib, response) - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) -end - -local function cumul_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) - end - end -end - -local function per_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(TOTAL_IMPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) - end -end - -local function energy_report_handler_factory(is_cumulative_report) - return function(driver, device, ib, response) - if not device:get_field(IMPORT_POLL_TIMER_SETTING_ATTEMPTED) then - set_poll_report_timer_and_schedule(device, is_cumulative_report) - end - if is_cumulative_report then - cumul_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - per_energy_imported_handler(driver, device, ib, response) - end - end -end - -local function initial_press_event_handler(driver, device, ib, response) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Receipt of an InitialPress event means we do not want to ignore the next MultiPressComplete event - -- or else we would potentially not create the expected button capability event - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) - elseif get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - elseif get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - -- if our button doesn't differentiate between short and long holds, do it in code by keeping track of the press down time - init_press(device, ib.endpoint_id) - end -end - --- if the device distinguishes a long press event, it will always be a "held" --- there's also a "long release" event, but this event is required to come first -local function long_press_event_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.held({state_change = true})) - if get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - -- Ignore the next MultiPressComplete event if it is sent as part of this "long press" event sequence - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, true) - end -end - -local function short_release_event_handler(driver, device, ib, response) - if not get_field_for_endpoint(device, SUPPORTS_MULTI_PRESS, ib.endpoint_id) then - if get_field_for_endpoint(device, EMULATE_HELD, ib.endpoint_id) then - emulate_held_event(device, ib.endpoint_id) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.button.pushed({state_change = true})) - end - end -end - -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end - end -end - -local function valve_state_attr_handler(driver, device, ib, response) - if ib.data.value == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.closed()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.valve.valve.open()) - end -end - -local function valve_level_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.level.level(ib.data.value)) - end -end - -local function multi_press_complete_event_handler(driver, device, ib, response) - -- in the case of multiple button presses - -- emit number of times, multiple presses have been completed - if ib.data and not get_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id) then - local press_value = ib.data.elements.total_number_of_presses_counted.value - --capability only supports up to 6 presses - if press_value < 7 then - local button_event = capabilities.button.button.pushed({state_change = true}) - if press_value == 2 then - button_event = capabilities.button.button.double({state_change = true}) - elseif press_value > 2 then - button_event = capabilities.button.button(string.format("pushed_%dx", press_value), {state_change = true}) - end - - device:emit_event_for_endpoint(ib.endpoint_id, button_event) - else - log.info(string.format("Number of presses (%d) not supported by capability", press_value)) - end - end - set_field_for_endpoint(device, IGNORE_NEXT_MPC, ib.endpoint_id, nil) -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local function battery_charge_level_attr_handler(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 power_source_attribute_list_handler(driver, device, ib, response) - local profile_name = "" - - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - 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 - profile_name = "button-battery" - break - elseif attr.value == 0x0E then - profile_name = "button-batteryLevel" - break - end - end - if profile_name ~= "" then - if #button_eps > 1 then - profile_name = string.format("%d-", #button_eps) .. profile_name - end - - if device.manufacturer_info.vendor_id == AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == AQARA_CLIMATE_SENSOR_W100_ID then - profile_name = profile_name .. "-temperature-humidity" - end - device:try_update_metadata({ profile = profile_name }) - end -end - -local function max_press_handler(driver, device, ib, response) - local max = ib.data.value or 1 --get max number of presses - device.log.debug("Device supports "..max.." presses") - -- capability only supports up to 6 presses - if max > 6 then - log.info("Device supports more than 6 presses") - max = 6 - end - local MSL = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) - local supportsHeld = tbl_contains(MSL, ib.endpoint_id) - local values = create_multi_press_values_list(max, supportsHeld) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.button.supportedButtonValues(values, {visibility = {displayed = false}})) -end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then device:subscribe() - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then - configure_buttons(device) - end - end -end - -local function device_added(driver, device) - -- refresh child devices to get an initial attribute state for OnOff in case child device - -- was created after the initial subscription report - if device.network_type == device_lib.NETWORK_TYPE_CHILD then - local req = clusters.OnOff.attributes.OnOff:read(device) - device:send(req) - end - - -- call device init in case init is not called after added due to device caching - device_init(driver, device) -end - -local function temperature_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local temp = measured_value / 100.0 - local unit = "C" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) end end -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local humidity = utils.round(measured_value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) - end -end - -local function fan_mode_handler(driver, device, ib, response) - if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("off")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.LOW then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("low")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.MEDIUM then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("medium")) - elseif ib.data.value == clusters.FanControl.attributes.FanMode.HIGH then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("high")) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode("auto")) - end -end - -local function fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes - if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.medium.NAME, - capabilities.fanMode.fanMode.high.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.high.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.medium.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.low.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.high.NAME, - capabilities.fanMode.fanMode.auto.NAME - } - else - supportedFanModes = { - capabilities.fanMode.fanMode.off.NAME, - capabilities.fanMode.fanMode.high.NAME - } - end - local event = capabilities.fanMode.supportedFanModes(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function fan_speed_percent_attr_handler(driver, device, ib, response) - if ib.data.value == nil or ib.data.value < 0 or ib.data.value > 100 then - return - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) -end - local matter_driver_template = { lifecycle_handlers = { - init = device_init, - added = device_added, - removed = device_removed, - infoChanged = info_changed, - doConfigure = do_configure, - driverSwitched = driver_switched + added = SwitchLifecycleHandlers.device_added, + doConfigure = SwitchLifecycleHandlers.do_configure, + driverSwitched = SwitchLifecycleHandlers.driver_switched, + infoChanged = SwitchLifecycleHandlers.info_changed, + init = SwitchLifecycleHandlers.device_init, + removed = SwitchLifecycleHandlers.device_removed, }, matter_handlers = { attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + [clusters.ColorControl.ID] = { + [clusters.ColorControl.attributes.ColorCapabilities.ID] = attribute_handlers.color_capabilities_handler, + [clusters.ColorControl.attributes.ColorMode.ID] = attribute_handlers.color_mode_handler, + [clusters.ColorControl.attributes.ColorTemperatureMireds.ID] = attribute_handlers.color_temperature_mireds_handler, + [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = attribute_handlers.color_temp_physical_mireds_bounds_factory(fields.COLOR_TEMP_MIN), -- max mireds = min kelvin + [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = attribute_handlers.color_temp_physical_mireds_bounds_factory(fields.COLOR_TEMP_MAX), -- min mireds = max kelvin + [clusters.ColorControl.attributes.CurrentHue.ID] = attribute_handlers.current_hue_handler, + [clusters.ColorControl.attributes.CurrentSaturation.ID] = attribute_handlers.current_saturation_handler, + [clusters.ColorControl.attributes.CurrentX.ID] = attribute_handlers.current_x_handler, + [clusters.ColorControl.attributes.CurrentY.ID] = attribute_handlers.current_y_handler, }, - [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler, - [clusters.LevelControl.attributes.MaxLevel.ID] = level_bounds_handler_factory(LEVEL_MAX), - [clusters.LevelControl.attributes.MinLevel.ID] = level_bounds_handler_factory(LEVEL_MIN), + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), }, - [clusters.ColorControl.ID] = { - [clusters.ColorControl.attributes.CurrentHue.ID] = hue_attr_handler, - [clusters.ColorControl.attributes.CurrentSaturation.ID] = sat_attr_handler, - [clusters.ColorControl.attributes.ColorTemperatureMireds.ID] = temp_attr_handler, - [clusters.ColorControl.attributes.CurrentX.ID] = x_attr_handler, - [clusters.ColorControl.attributes.CurrentY.ID] = y_attr_handler, - [clusters.ColorControl.attributes.ColorMode.ID] = color_mode_attr_handler, - [clusters.ColorControl.attributes.ColorCapabilities.ID] = color_cap_attr_handler, - [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MIN), -- max mireds = min kelvin - [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MAX), -- min mireds = max kelvin + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler, }, - [clusters.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler + [clusters.FanControl.ID] = { + [clusters.FanControl.attributes.FanMode.ID] = attribute_handlers.fan_mode_handler, + [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler }, - [clusters.OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, + [clusters.IlluminanceMeasurement.ID] = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, - [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler, + [clusters.LevelControl.ID] = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = attribute_handlers.level_control_current_level_handler, + [clusters.LevelControl.attributes.MaxLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MAX), + [clusters.LevelControl.attributes.MinLevel.ID] = attribute_handlers.level_bounds_handler_factory(fields.LEVEL_MIN), }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), + [clusters.OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_handler, }, - [clusters.ValveConfigurationAndControl.ID] = { - [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = valve_state_attr_handler, - [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = valve_level_attr_handler + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_attr_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, - }, - [clusters.Switch.ID] = { - [clusters.Switch.attributes.MultiPressMax.ID] = max_press_handler + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler + }, + [clusters.Switch.ID] = { + [clusters.Switch.attributes.MultiPressMax.ID] = attribute_handlers.multi_press_max_handler }, [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temperature_attr_handler, - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MIN), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MAX), + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MAX), + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), + }, + [clusters.ValveConfigurationAndControl.ID] = { + [clusters.ValveConfigurationAndControl.attributes.CurrentLevel.ID] = attribute_handlers.valve_configuration_current_level_handler, + [clusters.ValveConfigurationAndControl.attributes.CurrentState.ID] = attribute_handlers.valve_configuration_current_state_handler, }, - [clusters.FanControl.ID] = { - [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, - [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, - [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler - } }, event = { [clusters.Switch.ID] = { - [clusters.Switch.events.InitialPress.ID] = initial_press_event_handler, - [clusters.Switch.events.LongPress.ID] = long_press_event_handler, - [clusters.Switch.events.ShortRelease.ID] = short_release_event_handler, - [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler + [clusters.Switch.events.InitialPress.ID] = event_handlers.initial_press_handler, + [clusters.Switch.events.LongPress.ID] = event_handlers.long_press_handler, + [clusters.Switch.events.MultiPressComplete.ID] = event_handlers.multi_press_complete_handler, + [clusters.Switch.events.ShortRelease.ID] = event_handlers.short_release_handler, } }, - fallback = matter_handler, + fallback = switch_utils.matter_handler, }, subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, }, - [capabilities.switchLevel.ID] = { - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, }, [capabilities.colorControl.ID] = { clusters.ColorControl.attributes.ColorMode, @@ -1542,27 +215,28 @@ local matter_driver_template = { clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + }, + [capabilities.fanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanSpeedPercent.ID] = { + clusters.FanControl.attributes.PercentCurrent + }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue }, [capabilities.motionSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy }, - [capabilities.valve.ID] = { - clusters.ValveConfigurationAndControl.attributes.CurrentState - }, [capabilities.level.ID] = { clusters.ValveConfigurationAndControl.attributes.CurrentLevel }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff }, [capabilities.powerMeter.ID] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower @@ -1570,18 +244,19 @@ local matter_driver_template = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + }, [capabilities.temperatureMeasurement.ID] = { clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, clusters.TemperatureMeasurement.attributes.MaxMeasuredValue }, - [capabilities.fanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode + [capabilities.valve.ID] = { + clusters.ValveConfigurationAndControl.attributes.CurrentState }, - [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent - } }, subscribed_events = { [capabilities.button.ID] = { @@ -1592,71 +267,43 @@ local matter_driver_template = { }, }, capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_switch_level - }, [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = handle_set_color, - [capabilities.colorControl.commands.setHue.NAME] = handle_set_hue, - [capabilities.colorControl.commands.setSaturation.NAME] = handle_set_saturation, + [capabilities.colorControl.commands.setColor.NAME] = capability_handlers.handle_set_color, + [capabilities.colorControl.commands.setHue.NAME] = capability_handlers.handle_set_hue, + [capabilities.colorControl.commands.setSaturation.NAME] = capability_handlers.handle_set_saturation, }, [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature, + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = capability_handlers.handle_set_color_temperature, }, - [capabilities.valve.ID] = { - [capabilities.valve.commands.open.NAME] = handle_valve_open, - [capabilities.valve.commands.close.NAME] = handle_valve_close + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.handle_set_fan_mode + }, + [capabilities.fanSpeedPercent.ID] = { + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = capability_handlers.handle_fan_speed_set_percent }, [capabilities.level.ID] = { - [capabilities.level.commands.setLevel.NAME] = handle_set_level + [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level }, - [capabilities.fanMode.ID] = { - [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = capability_handlers.handle_switch_set_level + }, + [capabilities.valve.ID] = { + [capabilities.valve.commands.close.NAME] = capability_handlers.handle_valve_close, + [capabilities.valve.commands.open.NAME] = capability_handlers.handle_valve_open, }, - [capabilities.fanSpeedPercent.ID] = { - [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent - } - }, - supported_capabilities = { - capabilities.switch, - capabilities.switchLevel, - capabilities.colorControl, - capabilities.colorTemperature, - capabilities.level, - capabilities.motionSensor, - capabilities.illuminanceMeasurement, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport, - capabilities.valve, - capabilities.button, - capabilities.battery, - capabilities.batteryLevel, - capabilities.temperatureMeasurement, - capabilities.relativeHumidityMeasurement, - capabilities.fanMode, - capabilities.fanSpeedPercent }, + supported_capabilities = fields.supported_capabilities, sub_drivers = { - require("eve-energy"), - require("aqara-cube"), - require("third-reality-mk1") + require("sub_drivers.aqara_cube"), + require("sub_drivers.eve_energy"), + require("sub_drivers.third_reality_mk1") } } -function detect_matter_thing(device) - for _, capability in ipairs(matter_driver_template.supported_capabilities) do - if device:supports_capability(capability) then - return false - end - end - return device:supports_capability(capabilities.refresh) -end - local matter_driver = MatterDriver("matter-switch", matter_driver_template) log.info_with({hub_logs=true}, string.format("Starting %s driver, with dispatcher: %s", matter_driver.NAME, matter_driver.matter_dispatcher)) matter_driver:run() diff --git a/drivers/SmartThings/matter-switch/src/aqara-cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/aqara-cube/init.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua diff --git a/drivers/SmartThings/matter-switch/src/eve-energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/eve-energy/init.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua diff --git a/drivers/SmartThings/matter-switch/src/third-reality-mk1/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua similarity index 100% rename from drivers/SmartThings/matter-switch/src/third-reality-mk1/init.lua rename to drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index d9759b7add..30e3510e77 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -19,8 +19,8 @@ local t_utils = require "integration_test.utils" local version = require "version" if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ diff --git a/drivers/SmartThings/matter-switch/src/color_utils.lua b/drivers/SmartThings/matter-switch/src/utils/color_utils.lua similarity index 71% rename from drivers/SmartThings/matter-switch/src/color_utils.lua rename to drivers/SmartThings/matter-switch/src/utils/color_utils.lua index ce73b1eecd..410920258a 100644 --- a/drivers/SmartThings/matter-switch/src/color_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/color_utils.lua @@ -1,3 +1,17 @@ +-- Copyright 2025 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. + --TODO remove the usage of these color utils once 0.48.x has been distributed -- to all hubs. local color_utils = {} @@ -46,9 +60,9 @@ end --- Convert from x/y/Y to Hue/Saturation --- If every value is missing then [x, y, Y] = [0, 0, 1] --- ---- @param x number red in range [0x0000, 0xFFFF] ---- @param y number green in range [0x0000, 0xFFFF] ---- @param Y number blue in range [0x0000, 0xFFFF] +--- @param x number|nil red in range [0x0000, 0xFFFF] +--- @param y number|nil green in range [0x0000, 0xFFFF] +--- @param Y number|nil blue in range [0x0000, 0xFFFF] --- @returns number, number equivalent hue, saturation, level each in range [0,100]% color_utils.safe_xy_to_hsv = function(x, y, Y) local safe_x = x ~= nil and x / 65536 or 0 diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua new file mode 100644 index 0000000000..feb21ac193 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -0,0 +1,273 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "utils.embedded_cluster_utils" +local version = require "version" + +local fields = require "utils.switch_fields" +local switch_utils = require "utils.switch_utils" + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end + +local DeviceConfiguration = {} +local SwitchDeviceConfiguration = {} +local ButtonDeviceConfiguration = {} + +function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) + local profile + + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == child_ep then + -- Some devices report multiple device types which are a subset of + -- a superset device type (For example, Dimmable Light is a superset of + -- On/Off light). This mostly applies to the four light types, so we will want + -- to match the profile for the superset device type. This can be done by + -- matching to the device type with the highest ID + local id = 0 + for _, dt in ipairs(ep.device_types) do + id = math.max(id, dt.device_type_id) + end + profile = fields.device_type_profile_map[id] + break + end + end + + -- Check if device has an overridden child profile that differs from the profile that would match + -- the child's device type for the following two cases: + -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) + -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is + -- determined in the "for" loop above (e.g., light-binary) + -- 2. The selected profile for the child device matches the initial profile defined in + -- child_device_profile_overrides + for id, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do + for _, fingerprint in ipairs(vendor) do + if device.manufacturer_info.product_id == fingerprint.product_id and + ((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then + return fingerprint.target_profile + end + end + end + + -- default to "switch-binary" if no profile is found + return profile or "switch-binary" +end + +function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) + local num_switch_server_eps = 0 + local parent_child_device = false + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + table.sort(switch_eps) + for idx, ep in ipairs(switch_eps) do + if device:supports_server_cluster(clusters.OnOff.ID, ep) then + num_switch_server_eps = num_switch_server_eps + 1 + if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint + local name = string.format("%s %d", device.label, num_switch_server_eps) + local child_profile = SwitchDeviceConfiguration.assign_child_profile(device, ep) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep), + vendor_provided_label = name + } + ) + parent_child_device = true + if idx == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) + end + end + end + end + + -- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches + -- is only run once, but find_child function should be set on each driver init. + if parent_child_device then + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + end + + -- this is needed in initialize_buttons_and_switches + return num_switch_server_eps +end + +function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) + local cluster_id = 0 + for _, ep in ipairs(device.endpoints) do + -- main_endpoint only supports server cluster by definition of get_endpoints() + if main_endpoint == ep.endpoint_id then + for _, dt in ipairs(ep.device_types) do + -- no device type that is not in the switch subset should be considered. + if (fields.ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= fields.ON_OFF_COLOR_DIMMER_SWITCH_ID) then + cluster_id = math.max(cluster_id, dt.device_type_id) + end + end + break + end + end + + if fields.device_type_profile_map[cluster_id] then + device:try_update_metadata({profile = fields.device_type_profile_map[cluster_id]}) + end +end + +function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, num_button_eps) + local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep + if switch_utils.device_type_supports_button_switch_combination(device, main_endpoint) then + profile_name = "light-level-" .. profile_name + end + local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 + if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler + device:send(clusters.PowerSource.attributes.AttributeList:read(device)) + else + device:try_update_metadata({profile = profile_name}) + end +end + +function ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) + -- create component mapping on the main profile button endpoints + table.sort(button_eps) + local component_map = {} + component_map["main"] = main_endpoint + for component_num, ep in ipairs(button_eps) do + if ep ~= main_endpoint then + local button_component = "button" + if #button_eps > 1 then + button_component = button_component .. component_num + end + component_map[button_component] = ep + end + end + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + + +function ButtonDeviceConfiguration.configure_buttons(device) + local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + 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}) + local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) + + for _, ep in ipairs(ms_eps) do + if device.profile.components[switch_utils.endpoint_to_component(device, ep)] then + device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) + local supportedButtonValues_event + -- this ordering is important, since MSM & MSL devices must also support MSR + if switch_utils.tbl_contains(msm_eps, ep) then + supportedButtonValues_event = nil -- deferred to the max press handler + device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) + switch_utils.set_field_for_endpoint(device, fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) + elseif switch_utils.tbl_contains(msl_eps, ep) then + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) + elseif switch_utils.tbl_contains(msr_eps, ep) then + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}) + switch_utils.set_field_for_endpoint(device, fields.EMULATE_HELD, ep, true, {persist = true}) + else -- this switch endpoint only supports momentary switch, no release events + supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) + switch_utils.set_field_for_endpoint(device, fields.INITIAL_PRESS_ONLY, ep, true, {persist = true}) + end + + if supportedButtonValues_event then + device:emit_event_for_endpoint(ep, supportedButtonValues_event) + end + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + else + device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) + end + end +end + + +-- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- + +function DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) + local profile_found = false + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, #button_eps) + -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. + -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field + ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) + ButtonDeviceConfiguration.configure_buttons(device) + profile_found = true + end + + -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled + -- while building switch child profiles + local num_switch_server_eps = SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) + + -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. + -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. + -- Note: since their device type isn't supported, these devices join as a matter-thing. + if num_switch_server_eps > 0 and switch_utils.detect_matter_thing(device) then + SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) + profile_found = true + end + return profile_found +end + +function DeviceConfiguration.match_profile(driver, device) + local main_endpoint = switch_utils.find_default_endpoint(device) + -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. + local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + device:set_find_child(switch_utils.find_child) + end + if profile_found then + return + end + + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local level_eps = device:get_endpoints(clusters.LevelControl.ID) + local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) + local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) + local profile_name = nil + local level_support = "" + if #level_eps > 0 then + level_support = "-level" + end + if #energy_eps > 0 and #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" + elseif #energy_eps > 0 then + profile_name = "plug" .. level_support .. "-energy-powerConsumption" + elseif #power_eps > 0 then + profile_name = "plug" .. level_support .. "-power" + elseif #valve_eps > 0 then + profile_name = "water-valve" + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then + profile_name = profile_name .. "-level" + end + elseif #fan_eps > 0 then + profile_name = "light-color-level-fan" + end + if profile_name then + device:try_update_metadata({ profile = profile_name }) + end +end + +return { + DeviceCfg = DeviceConfiguration, + SwitchCfg = SwitchDeviceConfiguration, + ButtonCfg = ButtonDeviceConfiguration +} diff --git a/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua similarity index 69% rename from drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua rename to drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua index 66db6097c7..29af9feebd 100644 --- a/drivers/SmartThings/matter-switch/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua @@ -1,12 +1,26 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" local utils = require "st.utils" -- Include driver-side definitions when lua libs api version is < 11 local version = require "version" if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" - clusters.ValveConfigurationAndControl = require "ValveConfigurationAndControl" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end local embedded_cluster_utils = {} diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua new file mode 100644 index 0000000000..411c4c2104 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -0,0 +1,266 @@ +-- Copyright 2025 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 clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local version = require "version" + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" +end + +local SwitchFields = {} + +SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION +SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY + +SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" +SwitchFields.RECEIVED_X = "receivedX" +SwitchFields.RECEIVED_Y = "receivedY" +SwitchFields.HUESAT_SUPPORT = "huesatSupport" + + +SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 + +-- These values are a "sanity check" to check that values we are getting are reasonable +local COLOR_TEMPERATURE_KELVIN_MAX = 15000 +local COLOR_TEMPERATURE_KELVIN_MIN = 1000 +SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN +SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX + +SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 +SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 +SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 + + +-- DEVICE TYPES +SwitchFields.AGGREGATOR_DEVICE_TYPE_ID = 0x000E +SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 +SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID = 0x0101 +SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID = 0x010C +SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID = 0x010D +SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID = 0x010A +SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B +SwitchFields.ON_OFF_SWITCH_ID = 0x0103 +SwitchFields.ON_OFF_DIMMER_SWITCH_ID = 0x0104 +SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 +SwitchFields.MOUNTED_ON_OFF_CONTROL_ID = 0x010F +SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID = 0x0110 +SwitchFields.GENERIC_SWITCH_ID = 0x000F +SwitchFields.ELECTRICAL_SENSOR_ID = 0x0510 + +SwitchFields.device_type_profile_map = { + [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", + [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", + [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = "light-level-colorTemperature", + [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = "light-color-level", + [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = "plug-binary", + [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = "plug-level", + [SwitchFields.ON_OFF_SWITCH_ID] = "switch-binary", + [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = "switch-level", + [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", + [SwitchFields.MOUNTED_ON_OFF_CONTROL_ID] = "switch-binary", + [SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", +} + + +SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt + + +-- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for +-- devices that were joined to this driver as MCD devices before the transition +-- to join switch devices as parent-child. This value will exist in the device +-- table for devices that joined prior to this transition, and is also used for +-- button devices that require component mapping. +SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +SwitchFields.ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" +SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" +SwitchFields.COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" +SwitchFields.COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" +SwitchFields.COLOR_TEMP_MIN = "__color_temp_min" +SwitchFields.COLOR_TEMP_MAX = "__color_temp_max" +SwitchFields.LEVEL_BOUND_RECEIVED = "__level_bound_received" +SwitchFields.LEVEL_MIN = "__level_min" +SwitchFields.LEVEL_MAX = "__level_max" +SwitchFields.COLOR_MODE = "__color_mode" + +SwitchFields.updated_fields = { + { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, + { current_field_name = "__switch_intialized", updated_field_name = nil } +} + +SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION +SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY + + +SwitchFields.child_device_profile_overrides_per_vendor_id = { + [0x1321] = { + { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, + { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, + }, + [0x115F] = { + { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + } +} + +SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +SwitchFields.FIRST_IMPORT_REPORT_TIMESTAMP = "__first_import_report_timestamp" +SwitchFields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED = "__import_poll_timer_setting_attempted" +SwitchFields.IMPORT_REPORT_TIMEOUT = "__import_report_timeout" +SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" +SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +SwitchFields.RECURRING_IMPORT_REPORT_POLL_TIMER = "__recurring_import_report_poll_timer" +SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds +SwitchFields.SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" + +SwitchFields.START_BUTTON_PRESS = "__start_button_press" +SwitchFields.TIMEOUT_THRESHOLD = 10 --arbitrary timeout +SwitchFields.HELD_THRESHOLD = 1 + +-- this is the number of buttons for which we have a static profile already made +SwitchFields.STATIC_BUTTON_PROFILE_SUPPORTED = {1, 2, 3, 4, 5, 6, 7, 8, 9} + +-- Some switches will send a MultiPressComplete event as part of a long press sequence. Normally the driver will create a +-- button capability event on receipt of MultiPressComplete, but in this case that would result in an extra event because +-- the "held" capability event is generated when the LongPress event is received. The IGNORE_NEXT_MPC flag is used +-- to tell the driver to ignore MultiPressComplete if it is received after a long press to avoid this extra event. +SwitchFields.IGNORE_NEXT_MPC = "__ignore_next_mpc" + +-- These are essentially storing the supported features of a given endpoint +-- TODO: add an is_feature_supported_for_endpoint function to matter.device that takes an endpoint +SwitchFields.EMULATE_HELD = "__emulate_held" -- for non-MSR (MomentarySwitchRelease) devices we can emulate this on the software side +SwitchFields.SUPPORTS_MULTI_PRESS = "__multi_button" -- for MSM devices (MomentarySwitchMultiPress), create an event on receipt of MultiPressComplete +SwitchFields.INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS (MomentarySwitch), but not MSR (MomentarySwitchRelease) + +SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" +SwitchFields.TEMP_MIN = "__temp_min" +SwitchFields.TEMP_MAX = "__temp_max" + +SwitchFields.AQARA_MANUFACTURER_ID = 0x115F +SwitchFields.AQARA_CLIMATE_SENSOR_W100_ID = 0x2004 + +SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second +-- When sent with a command, these options mask and override bitmaps cause the command +-- to take effect when the switch/light is off. +SwitchFields.OPTIONS_MASK = 0x01 +SwitchFields.OPTIONS_OVERRIDE = 0x01 + + +SwitchFields.supported_capabilities = { + capabilities.battery, + capabilities.batteryLevel, + capabilities.button, + capabilities.colorControl, + capabilities.colorTemperature, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanSpeedPercent, + capabilities.illuminanceMeasurement, + capabilities.level, + capabilities.motionSensor, + capabilities.powerMeter, + capabilities.powerConsumptionReport, + capabilities.relativeHumidityMeasurement, + capabilities.switch, + capabilities.switchLevel, + capabilities.temperatureMeasurement, + capabilities.valve, +} + +SwitchFields.device_type_attribute_map = { + [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff + }, + [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds + }, + [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY + }, + [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff + }, + [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [SwitchFields.ON_OFF_SWITCH_ID] = { + clusters.OnOff.attributes.OnOff + }, + [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel + }, + [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY + }, + [SwitchFields.GENERIC_SWITCH_ID] = { + clusters.PowerSource.attributes.BatPercentRemaining, + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete + }, + [SwitchFields.ELECTRICAL_SENSOR_ID] = { + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + } +} + +return SwitchFields \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua new file mode 100644 index 0000000000..e3f9e667f9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -0,0 +1,206 @@ +-- Copyright 2025 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 fields = require "utils.switch_fields" +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local log = require "log" + +local utils = {} + +function utils.tbl_contains(array, value) + for _, element in ipairs(array) do + if element == value then + return true + end + end + return false +end + +function utils.convert_huesat_st_to_matter(val) + return st_utils.clamp_value(math.floor((val * 0xFE) / 100.0 + 0.5), fields.CURRENT_HUESAT_ATTR_MIN, fields.CURRENT_HUESAT_ATTR_MAX) +end + +function utils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function utils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function utils.mired_to_kelvin(value, minOrMax) + if value == 0 then -- shouldn't happen, but has + value = 1 + log.warn(string.format("Received a color temperature of 0 mireds. Using a color temperature of 1 mired to avoid divide by zero")) + end + -- We divide inside the rounding and multiply outside of it because we expect these + -- bounds to be multiples of 100. For the maximum mired value (minimum K value), + -- add 1 before converting and round up to nearest hundreds. For the minimum mired + -- (maximum K value) value, subtract 1 before converting and round down to nearest + -- hundreds. Note that 1 is added/subtracted from the mired value in order to avoid + -- rounding errors from the conversion of Kelvin to mireds. + local kelvin_step_size = 100 + local rounding_value = 0.5 + if minOrMax == fields.COLOR_TEMP_MIN then + return st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value + 1)) + rounding_value) * kelvin_step_size + elseif minOrMax == fields.COLOR_TEMP_MAX then + return st_utils.round(fields.MIRED_KELVIN_CONVERSION_CONSTANT / (kelvin_step_size * (value - 1)) - rounding_value) * kelvin_step_size + else + log.warn_with({hub_logs = true}, "Attempted to convert temperature unit for an undefined value") + end +end + +function utils.check_field_name_updates(device) + for _, field in ipairs(fields.updated_fields) do + if device:get_field(field.current_field_name) then + if field.updated_field_name ~= nil then + device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) + end + device:set_field(field.current_field_name, nil) + end + end +end + +--- device_type_supports_button_switch_combination helper function used to check +--- whether the device type for an endpoint is currently supported by a profile for +--- combination button/switch devices. +function utils.device_type_supports_button_switch_combination(device, endpoint_id) + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID then + for _, fingerprint in ipairs(fields.child_device_profile_overrides_per_vendor_id[0x115F]) do + if device.manufacturer_info.product_id == fingerprint.product_id then + return false -- For Aqara Dimmer Switch with Button. + end + end + return true + end + end + end + end + return false +end + +--- find_default_endpoint is a helper function to handle situations where +--- device does not have endpoint ids in sequential order from 1 +function utils.find_default_endpoint(device) + if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and + device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then + -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 + return device.MATTER_DEFAULT_ENDPOINT + end + + local switch_eps = device:get_endpoints(clusters.OnOff.ID) + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + + local get_first_non_zero_endpoint = function(endpoints) + table.sort(endpoints) + for _,ep in ipairs(endpoints) do + if ep ~= 0 then -- 0 is the matter RootNode endpoint + return ep + end + end + return nil + end + + -- Return the first switch endpoint as the default endpoint if no button endpoints are present + if #button_eps == 0 and #switch_eps > 0 then + return get_first_non_zero_endpoint(switch_eps) + end + + -- Return the first button endpoint as the default endpoint if no switch endpoints are present + if #switch_eps == 0 and #button_eps > 0 then + return get_first_non_zero_endpoint(button_eps) + end + + -- If both switch and button endpoints are present, check the device type on the main switch + -- endpoint. If it is not a supported device type, return the first button endpoint as the + -- default endpoint. + if #switch_eps > 0 and #button_eps > 0 then + local main_endpoint = get_first_non_zero_endpoint(switch_eps) + if utils.device_type_supports_button_switch_combination(device, main_endpoint) then + return main_endpoint + else + device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") + return get_first_non_zero_endpoint(button_eps) + end + end + + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return device.MATTER_DEFAULT_ENDPOINT +end + +function utils.component_to_endpoint(device, component) + local map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + if map[component] then + return map[component] + end + return utils.find_default_endpoint(device) +end + +function utils.endpoint_to_component(device, ep) + local map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + for component, endpoint in pairs(map) do + if endpoint == ep then + return component + end + end + return "main" +end + +function utils.find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +end + +-- Fallback handler for responses that dont have their own handler +function utils.matter_handler(driver, device, response_block) + device.log.info(string.format("Fallback handler for %s", response_block)) +end + +--helper function to create list of multi press values +function utils.create_multi_press_values_list(size, supportsHeld) + local list = {"pushed", "double"} + if supportsHeld then table.insert(list, "held") end + -- add multi press values of 3 or greater to the list + for i=3, size do + table.insert(list, string.format("pushed_%dx", i)) + end + return list +end + + +function utils.detect_bridge(device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.AGGREGATOR_DEVICE_TYPE_ID then + return true + end + end + end + return false +end + +function utils.detect_matter_thing(device) + for _, capability in ipairs(fields.supported_capabilities) do + if device:supports_capability(capability) then + return false + end + end + return device:supports_capability(capabilities.refresh) +end + +return utils From 2c3c5952c27b909fd02293734c3a15b96903d9f1 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 29 Sep 2025 13:12:00 -0500 Subject: [PATCH 167/449] refactor powerConsumption report framework (#2426) --- .../SmartThings/matter-energy/src/init.lua | 309 +++++----------- .../src/test/test_battery_storage.lua | 104 ++---- .../src/test/test_evse_energy_meas.lua | 75 +--- .../src/test/test_solar_power.lua | 97 +----- .../generic_handlers/attribute_handlers.lua | 12 +- .../power_consumption_report.lua | 114 ------ .../SmartThings/matter-switch/src/init.lua | 16 +- .../src/sub_drivers/eve_energy/init.lua | 77 ++-- .../src/test/test_aqara_light_switch_h2.lua | 24 +- .../src/test/test_electrical_sensor.lua | 329 ++---------------- .../src/test/test_eve_energy.lua | 39 +-- .../matter-switch/src/utils/switch_fields.lua | 5 - .../matter-switch/src/utils/switch_utils.lua | 40 ++- .../matter-thermostat/src/init.lua | 192 ++++------ .../src/test/test_matter_heat_pump.lua | 163 ++------- .../src/test/test_matter_water_heater.lua | 57 ++- 16 files changed, 412 insertions(+), 1241 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua diff --git a/drivers/SmartThings/matter-energy/src/init.lua b/drivers/SmartThings/matter-energy/src/init.lua index 69c28f8638..da56397ec3 100644 --- a/drivers/SmartThings/matter-energy/src/init.lua +++ b/drivers/SmartThings/matter-energy/src/init.lua @@ -36,19 +36,17 @@ end local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local SUPPORTED_EVSE_MODES_MAP = "__supported_evse_modes_map" local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP = "__supported_device_energy_management_modes_map" -local RECURRING_REPORT_POLL_TIMER = "__recurring_report_poll_timer" -local RECURRING_POLL_TIMER = "__recurring_poll_timer" -local LAST_REPORTED_TIME = "__last_reported_time" -local POWER_CONSUMPTION_REPORT_TIME_INTERVAL = "__pcr_time_interval" -local DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED = "__timer_interval_considered" + +local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +local LAST_EXPORTED_REPORT_TIMESTAMP = "__last_exported_report_timestamp" +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + -- total in case there are multiple electrical sensors local TOTAL_CUMULATIVE_ENERGY_IMPORTED = "__total_cumulative_energy_imported" local TOTAL_CUMULATIVE_ENERGY_EXPORTED = "__total_cumulative_energy_exported" local TOTAL_ACTIVE_POWER = "__total_active_power" -local TIMER_REPEAT = (1 * 60) -- 1 minute -local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes -local MAX_REPORT_TIMEOUT = (30 * 60) local MAX_CHARGING_CURRENT_CONSTRAINT = 80000 -- In v1.3 release of stack, this check for 80 A is performed. local EVSE_DEVICE_TYPE_ID = 0x050C @@ -135,17 +133,17 @@ local function tbl_contains(array, value) end local get_total = function(map) + local total_value = 0 if type(map) == "table" then - local total_value = 0 for _, value in pairs(map) do if type(value) == "number" then total_value = total_value + value end end - return total_value else log.debug("get_total: 'map' should be of type table") end + return total_value end -- MAPS -- @@ -193,145 +191,30 @@ local BATTERY_CHARGING_STATE_MAP = { [clusters.PowerSource.types.BatChargeStateEnum.IS_AT_FULL_CHARGE] = capabilities.chargingState.chargingState.fullyCharged, } --- Matter Handlers -local function read_cumulative_energy(device) - local cumul_imp_eps = embedded_cluster_utils.get_endpoints( - device, clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.IMPORTED_ENERGY } - ) - if cumul_imp_eps and #cumul_imp_eps > 0 then - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device) - device:send(read_req) - end - - -- read energy exported only in case of Solar Power / Battery Storage device. - local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} - local eps_to_read = {} - utils.merge(eps_to_read, battery_storage_eps) - utils.merge(eps_to_read, solar_power_eps) - if #eps_to_read > 0 then - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, eps_to_read[1]) - for i, ep in ipairs(eps_to_read) do - if i > 1 then - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(device, ep)) - end - end - device:send(read_req) - end -end - -local function create_poll_schedule(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - return - end - - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) or {} - if #cumul_eps == 0 then - return - end - read_cumulative_energy(device) - -- Read cumulative energy imported/exported attributes every minute - local timer = device.thread:call_on_schedule(TIMER_REPEAT, function() - read_cumulative_energy(device) - end, "polling_schedule_timer") - - device:set_field(RECURRING_POLL_TIMER, timer) -end - -local report_energy_to_app = function(device, comp, energy_map, startTime, endTime) - local component = device.profile.components[comp] - local total_cumulative_energy = get_total(energy_map) or 0 - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state( - comp, capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME - ) or { energy = 0 } - local deltaEnergyWh = math.max(total_cumulative_energy - previousTotalConsumptionWh.energy, 0.0) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = total_cumulative_energy - })) -end - -local function create_poll_report_schedule(device) - local polling_schedule_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) - if polling_schedule_timer ~= nil then - return - end - - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local pcr_interval = device:get_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL) or REPORT_TIMEOUT - - local timer = device.thread:call_on_schedule(pcr_interval, function() - local current_time = os.time() - local last_time = device:get_field(LAST_REPORTED_TIME) or 0 - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED) - local cumulative_energy_exported = device:get_field(TOTAL_CUMULATIVE_ENERGY_EXPORTED) - device:set_field(LAST_REPORTED_TIME, current_time, { persist = true }) - local startTime = epoch_to_iso8601(last_time) - local endTime = epoch_to_iso8601(current_time - 1) - - if cumulative_energy_imported ~= nil then - local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID) or {} - local comp_id = "importedEnergy" - if #evse_eps > 0 then - comp_id = "main" - end - report_energy_to_app(device, comp_id, cumulative_energy_imported, startTime, endTime) - end - - -- If energy exported is set, it must be for Solar Power / Battery Storage Device. - if cumulative_energy_exported ~= nil then - report_energy_to_app(device, "exportedEnergy", cumulative_energy_exported, startTime, endTime) - end - end, "polling_report_schedule_timer") - - device:set_field(RECURRING_REPORT_POLL_TIMER, timer) -end - -local function create_poll_schedules_for_cumulative_energy_reports(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - create_poll_schedule(device) - create_poll_report_schedule(device) -end - -local function delete_reporting_timer(device) - local reporting_poll_timer = device:get_field(RECURRING_REPORT_POLL_TIMER) - if reporting_poll_timer ~= nil then - device.thread:cancel_timer(reporting_poll_timer) - device:set_field(RECURRING_REPORT_POLL_TIMER, nil) - end -end - -local function delete_poll_schedules(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - device.thread:cancel_timer(poll_timer) - device:set_field(RECURRING_POLL_TIMER, nil) - end - delete_reporting_timer(device) -end - -- Lifecycle Handlers -- local function device_init(driver, device) device:subscribe() device:set_endpoint_to_component_fn(endpoint_to_component) device:set_component_to_endpoint_fn(component_to_endpoint) - create_poll_schedules_for_cumulative_energy_reports(device) - local current_time = os.time() - local current_time_iso8601 = epoch_to_iso8601(current_time) -- emit current time by default - device:emit_event(capabilities.evseChargingSession.targetEndTime(current_time_iso8601)) + local evse_eps = get_endpoints_for_dt(device, EVSE_DEVICE_TYPE_ID) or {} + if #evse_eps > 0 then + local current_time = os.time() + local current_time_iso8601 = epoch_to_iso8601(current_time) + device:emit_event(capabilities.evseChargingSession.targetEndTime(current_time_iso8601)) + end + + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + end end local function device_added(driver, device) @@ -381,11 +264,10 @@ local function info_changed(driver, device) end end device:subscribe() - create_poll_schedules_for_cumulative_energy_reports(device) end local function device_removed(driver, device) - delete_poll_schedules(device) + device.log.info("device removed") end -- Matter Handlers -- @@ -565,93 +447,88 @@ local function device_energy_mgmt_mode_attr_handler(driver, device, ib, response end end -local function report_energy_meter(device, energy_map_id) - --report energyMeter for Solar Power and Battery Storage devices only. - local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} - local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local energy_map = device:get_field(energy_map_id) or {} - local total_energy = get_total(energy_map) or 0 +local function report_power_consumption_to_st_energy(device, component, latest_total_imported_energy_wh) + local current_time = os.time() - if #battery_storage_eps > 0 then - local component = device.profile.components["importedEnergy"] - if energy_map_id == TOTAL_CUMULATIVE_ENERGY_EXPORTED then - component = device.profile.components["exportedEnergy"] - end - device:emit_component_event(component, capabilities.energyMeter.energy({value = total_energy, unit = "Wh"})) + local last_report_timestamp_field = component.id == "exportedEnergy" and LAST_EXPORTED_REPORT_TIMESTAMP or LAST_IMPORTED_REPORT_TIMESTAMP + local last_time = device:get_field(last_report_timestamp_field) or 0 + -- Ensure that the previous report was sent at least 15 minutes ago + if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - -- energyMeter in Solar Power devices must report exported energy only. - if #solar_power_eps > 0 and energy_map_id == TOTAL_CUMULATIVE_ENERGY_EXPORTED then - device:emit_event(capabilities.energyMeter.energy({value = total_energy, unit = "Wh"})) + device:set_field(last_report_timestamp_field, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end -local function cumulative_energy_handler(energy_map_id) - return function(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local cumulative_energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - local total_cumulative_energy = device:get_field(energy_map_id) or {} - - -- in case there are multiple electrical sensors store them in a table. - total_cumulative_energy[endpoint_id] = cumulative_energy_Wh - device:set_field(energy_map_id, total_cumulative_energy, { persist = true }) - report_energy_meter(device, energy_map_id) +local function get_component_for_energy_reports(device, cumulative_import_or_export_field) + local energyMeter_component, powerConsumption_component + if cumulative_import_or_export_field == TOTAL_CUMULATIVE_ENERGY_EXPORTED then -- this is an export report + energyMeter_component = "exportedEnergy" + powerConsumption_component = "exportedEnergy" + if #get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "main" + powerConsumption_component = "exportedEnergy" + end + else + energyMeter_component = "main" + powerConsumption_component = "main" + if #get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "importedEnergy" + powerConsumption_component = "importedEnergy" + elseif #get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) > 0 then + energyMeter_component = "N/A" -- do not send cumulative import reports for solar power end end + return energyMeter_component, powerConsumption_component end - -local function periodic_energy_handler(energy_map_id) +local function energy_report_handler_factory(is_cumulative_report, cumulative_import_or_export_field) return function(driver, device, ib, response) - local endpoint_id = ib.endpoint_id - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) - end + if not ib.data then return + elseif version.api < 11 then clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) end - local start_timestamp = ib.data.elements.start_timestamp.value or 0 - local end_timestamp = ib.data.elements.end_timestamp.value or 0 - - local device_reporting_time_interval = end_timestamp - start_timestamp - if not device:get_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED) and device_reporting_time_interval > REPORT_TIMEOUT then - -- This is a one time setup in order to consider a larger time interval if the interval the device chooses to report is greater than 15 minutes. - device_reporting_time_interval = utils.clamp_value(device_reporting_time_interval, REPORT_TIMEOUT, MAX_REPORT_TIMEOUT) - device:set_field(DEVICE_REPORTED_TIME_INTERVAL_CONSIDERED, true, {persist=true}) - device:set_field(POWER_CONSUMPTION_REPORT_TIME_INTERVAL, device_reporting_time_interval, {persist = true}) - delete_reporting_timer(device) - create_poll_report_schedule(device) - end + local endpoint_id = string.format(ib.endpoint_id) + local total_cumulative_energy = device:get_field(cumulative_import_or_export_field) or {} + local energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - if tbl_contains(cumul_eps, endpoint_id) then - -- Since cluster in this endpoint supports both CUME & PERE features, we will prefer - -- cumulative_energy_handler to handle the energy report for this endpoint over periodic_energy_handler. - return + if not is_cumulative_report then + if device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) ~= true then + return -- if this is a periodic report and cumulative reports ARE supported by the device, ignore the report altogether. end + energy_Wh = energy_Wh + (total_cumulative_energy[endpoint_id] or 0) -- handle the periodic report + end + total_cumulative_energy[endpoint_id] = energy_Wh -- in the case that there are multiple electrical sensors, store them in a table. + device:set_field(cumulative_import_or_export_field, total_cumulative_energy, { persist = true }) - endpoint_id = string.format(ib.endpoint_id) - local energy_Wh = utils.round(ib.data.elements.energy.value / 1000) -- convert mWh to Wh - local total_cumulative_energy = device:get_field(energy_map_id) or {} - - -- in case there are multiple electrical sensors store them in a table. - total_cumulative_energy[endpoint_id] = total_cumulative_energy[endpoint_id] or 0 - total_cumulative_energy[endpoint_id] = total_cumulative_energy[endpoint_id] + energy_Wh - device:set_field(energy_map_id, total_cumulative_energy, { persist = true }) - report_energy_meter(device, energy_map_id) + local summed_total_energy = get_total(total_cumulative_energy) + local energyMeter_component, powerConsumption_component = get_component_for_energy_reports(device, cumulative_import_or_export_field) + if device.profile.components[energyMeter_component] and device:supports_capability(capabilities.energyMeter) then + device:emit_component_event(device.profile.components[energyMeter_component], capabilities.energyMeter.energy({value = summed_total_energy, unit = "Wh"})) + end + if device.profile.components[powerConsumption_component] and device:supports_capability(capabilities.powerConsumptionReport) then + report_power_consumption_to_st_energy(device, device.profile.components[powerConsumption_component], summed_total_energy) end end end local function active_power_handler(driver, device, ib, response) - local battery_storage_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} - local solar_power_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} + local battery_storage_eps = get_endpoints_for_dt(device, BATTERY_STORAGE_DEVICE_TYPE_ID) or {} + local solar_power_eps = get_endpoints_for_dt(device, SOLAR_POWER_DEVICE_TYPE_ID) or {} -- Consider only Solar Power / Battery Storage devices and sum up in case there are multiple endpoints. if (tbl_contains(solar_power_eps, ib.endpoint_id) or tbl_contains(battery_storage_eps, ib.endpoint_id)) and ib.data.value then local endpoint_id = string.format(ib.endpoint_id) @@ -788,10 +665,10 @@ matter_driver_template = { [clusters.DeviceEnergyManagementMode.attributes.CurrentMode.ID] = device_energy_mgmt_mode_attr_handler, }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_IMPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = cumulative_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = periodic_energy_handler(TOTAL_CUMULATIVE_ENERGY_EXPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true, TOTAL_CUMULATIVE_ENERGY_IMPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false, TOTAL_CUMULATIVE_ENERGY_IMPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported.ID] = energy_report_handler_factory(true, TOTAL_CUMULATIVE_ENERGY_EXPORTED), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyExported.ID] = energy_report_handler_factory(false, TOTAL_CUMULATIVE_ENERGY_EXPORTED), }, [clusters.PowerSource.ID] = { [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, 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 27f8585185..e2c3cbd44c 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -81,16 +81,6 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -201,18 +191,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported every 15 minutes", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, @@ -226,52 +205,42 @@ test.register_coroutine_test( })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("importedEnergy", + capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 100 + }) + ) + ) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 300000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --300Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --400Wh test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.energyMeter.energy({ - value = 300, unit = "Wh" + value = 400, unit = "Wh" })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.powerConsumptionReport.powerConsumption({ - energy = 300, - deltaEnergy = 300, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("importedEnergy", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 100, - deltaEnergy = 100, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 400 + }) + ) ) test.wait_for_events() - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, BATTERY_STORAGE_EP) - }) + test.mock_time.advance_time(2000) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, @@ -284,6 +253,16 @@ test.register_coroutine_test( value = 200, unit = "Wh" }))) + test.socket.capability:__expect_send( + mock_device:generate_test_message("importedEnergy", + capabilities.powerConsumptionReport.powerConsumption({ + energy = 200, + deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z" + })) + ) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, @@ -296,36 +275,19 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", capabilities.powerConsumptionReport.powerConsumption({ energy = 400, - deltaEnergy = 100, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" - })) - ) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("importedEnergy", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 200, - deltaEnergy = 100, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" + deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z" })) ) - - test.wait_for_events() end, { test_init = function() test_init() - 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 } ) 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 84c431ffe2..610c443625 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 @@ -101,11 +101,6 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.evseChargingSession.targetEndTime("1970-01-01T00:00:00Z"))) @@ -125,92 +120,34 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Ensure timers are created for the device", - function() - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer ~= nil, "poll_timer should exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - end -) - -test.register_coroutine_test( - "Ensure timers are created for the device", - function() - test.socket.matter:__set_channel_ordering("relaxed") - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer == nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer == nil, "report_poll_timer should not exist") - end -) - -test.register_coroutine_test( - "Ensure that every 60 seconds the driver reads the CumulativeEnergyImported attribute for both endpoints", + "Ensure the total accumulated powerConsumption for both endpoints is reported", function() - test.mock_time.advance_time(60) - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the total accumulated powerConsumption for both endpoints is reported every 15 minutes", - function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - test.socket.matter:__expect_send({ - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP_ONE, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + test.wait_for_events() + test.mock_time.advance_time(901) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP_TWO, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 250, - deltaEnergy = 250, + deltaEnergy = 0.0, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" + ["end"] = "1970-01-01T00:15:00Z" })) ) - - test.wait_for_events() end, { test_init = function() test_init() - 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 } ) 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 87fde17a23..8c5e4c6ff8 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -87,13 +87,6 @@ local function test_init() 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 }) - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) - - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure"}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -133,60 +126,9 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Ensure timers are created for the device and terminated on removed", - function() - test.socket.matter:__set_channel_ordering("relaxed") - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer ~= nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - - local poll_timer = mock_device:get_field("__recurring_poll_timer") - assert(poll_timer == nil, "poll_timer should not exist") - - local report_poll_timer = mock_device:get_field("__recurring_report_poll_timer") - assert(report_poll_timer == nil, "report_poll_timer should not exist") - end -) - -test.register_coroutine_test( - "Ensure that every 60 seconds the driver reads the CumulativeEnergyExported attribute for both endpoints", - function() - test.mock_time.advance_time(60) - test.socket.matter:__set_channel_ordering("relaxed") - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) - test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported every 15 minutes", + "Ensure the total cumulative energy exported powerConsumption for both endpoints is reported", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.capability:__set_channel_ordering("relaxed") - - local read_req = clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_ONE) - read_req:merge(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyExported:read(mock_device, SOLAR_POWER_EP_TWO)) - - test.socket.matter:__expect_send({ - mock_device.id, - read_req - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, @@ -200,37 +142,32 @@ test.register_coroutine_test( })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("exportedEnergy", + capabilities.powerConsumptionReport.powerConsumption({ + energy = 100, + deltaEnergy = 0.0, + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z" + }) + ) + ) + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_TWO, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.energyMeter.energy({ - value = 250, unit = "Wh" - })) - ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( - mock_device:generate_test_message("exportedEnergy", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 250, - deltaEnergy = 250, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) + mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ + value = 250, unit = "Wh" + })) ) - - test.wait_for_events() end, { test_init = function() test_init() - 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 } ) diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua index 3976c2c8a3..ad8a74a00f 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -22,8 +22,6 @@ local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" local color_utils = require "utils.color_utils" -local power_consumption_reporting = require "generic_handlers.power_consumption_report" - local AttributeHandlers = {} -- [[ ON OFF CLUSTER ATTRIBUTES ]] -- @@ -291,6 +289,7 @@ function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, res -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) end + switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end @@ -301,14 +300,19 @@ function AttributeHandlers.per_energy_imported_handler(driver, device, ib, respo local summed_energy_report = latest_energy_report + watt_hour_value device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) + switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end function AttributeHandlers.energy_imported_factory(is_cumulative_report) return function(driver, device, ib, response) - if not device:get_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED) then - power_consumption_reporting.set_poll_report_timer_and_schedule(device, is_cumulative_report) + -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster + local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 + local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) + if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then + return end + if is_cumulative_report then AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua deleted file mode 100644 index 8692d7b262..0000000000 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/power_consumption_report.lua +++ /dev/null @@ -1,114 +0,0 @@ --- Copyright 2025 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 clusters = require "st.matter.clusters" -local fields = require "utils.switch_fields" -local embedded_cluster_utils = require "utils.embedded_cluster_utils" -local version = require "version" - -local PowerConsumptionReporting = {} - --- Include driver-side definitions when lua libs api version is < 11 -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" -end - --- [[ POWER CONSUMPTION REPORT HELPER FUNCTIONS ]] -- - --- Return an ISO-8061 timestamp in UTC -local function iso8061Timestamp(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - --- Emit the capability event capturing the latest energy delta and timestamps -local function send_import_poll_report(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - else - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = iso8061Timestamp(last_time), - ["end"] = iso8061Timestamp(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) - end -end - --- Set the poll report schedule on the timer defined by IMPORT_REPORT_TIMEOUT -local function create_poll_report_schedule(device) - local import_timer = device.thread:call_on_schedule( - device:get_field(fields.IMPORT_REPORT_TIMEOUT), function() - send_import_poll_report(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) - end, "polling_import_report_schedule_timer" - ) - device:set_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER, import_timer) -end - -function PowerConsumptionReporting.set_poll_report_timer_and_schedule(device, is_cumulative_report) - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - if #cumul_eps == 0 then - device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = true}) - end - if #cumul_eps > 0 and not is_cumulative_report then - return - elseif not device:get_field(fields.SUBSCRIPTION_REPORT_OCCURRED) then - device:set_field(fields.SUBSCRIPTION_REPORT_OCCURRED, true) - elseif not device:get_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP) then - device:set_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP, os.time()) - else - local first_timestamp = device:get_field(fields.FIRST_IMPORT_REPORT_TIMESTAMP) - local second_timestamp = os.time() - local report_interval_secs = second_timestamp - first_timestamp - device:set_field(fields.IMPORT_REPORT_TIMEOUT, math.max(report_interval_secs, fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL)) - -- the poll schedule is only needed for devices that support powerConsumption - -- and enable powerConsumption when energy management is defined in root endpoint(0). - if device:supports_capability(capabilities.powerConsumptionReport) or - device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then - create_poll_report_schedule(device) - end - device:set_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED, true) - end -end - -function PowerConsumptionReporting.delete_import_poll_schedule(device) - local import_poll_timer = device:get_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER) - if import_poll_timer then - device.thread:cancel_timer(import_poll_timer) - device:set_field(fields.RECURRING_IMPORT_REPORT_POLL_TIMER, nil) - device:set_field(fields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED, nil) - end -end - -return PowerConsumptionReporting diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 6deb8ab603..bbf9ca24ee 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -18,6 +18,7 @@ local device_lib = require "st.device" local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" +local embedded_cluster_utils = require "utils.embedded_cluster_utils" local fields = require "utils.switch_fields" local switch_utils = require "utils.switch_utils" @@ -29,7 +30,6 @@ local button_cfg = cfg.ButtonCfg local attribute_handlers = require "generic_handlers.attribute_handlers" local event_handlers = require "generic_handlers.event_handlers" local capability_handlers = require "generic_handlers.capability_handlers" -local power_consumption_reporting = require "generic_handlers.power_consumption_report" -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then @@ -75,7 +75,6 @@ end function SwitchLifecycleHandlers.device_removed(driver, device) device.log.info("device removed") - power_consumption_reporting.delete_import_poll_schedule(device) end function SwitchLifecycleHandlers.device_init(driver, device) @@ -112,8 +111,19 @@ function SwitchLifecycleHandlers.device_init(driver, device) end end end - device:subscribe() + + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + end end end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 410ace0c2c..04c2d7fc1d 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -38,11 +38,9 @@ local PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT = 0x130A000E local RECURRING_POLL_TIMER = "RECURRING_POLL_TIMER" local TIMER_REPEAT = (1 * 60) -- Run the timer each minute --- Timer to report the power consumption every 15 minutes to satisfy the ST energy requirement -local RECURRING_REPORT_POLL_TIMER = "RECURRING_REPORT_POLL_TIMER" local LAST_REPORT_TIME = "LAST_REPORT_TIME" local LATEST_TOTAL_CONSUMPTION_WH = "LATEST_TOTAL_CONSUMPTION_WH" -local REPORT_TIMEOUT = (15 * 60) -- Report the value each 15 minutes +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds ------------------------------------------------------------------------------------- @@ -61,7 +59,7 @@ end -- Return a ISO 8061 formatted timestamp in UTC (Z) -- @return e.g. 2022-02-02T08:00:00Z -local function iso8061Timestamp(time) +local function epoch_to_iso8601(time) return os.date("!%Y-%m-%dT%TZ", time) end @@ -116,43 +114,33 @@ local function delete_poll_schedule(device) end end +local function report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(LAST_REPORT_TIME) or 0 -local function create_poll_report_schedule(device) - -- the poll schedule is only needed for devices that support powerConsumption - if not device:supports_capability(capabilities.powerConsumptionReport) then + -- Ensure that the previous report was sent at least 15 minutes ago + if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local timer = device.thread:call_on_schedule(REPORT_TIMEOUT, function() - local current_time = os.time() - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local latestTotalConsumptionWH = device:get_field(LATEST_TOTAL_CONSUMPTION_WH) or 0 - - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - - local deltaEnergyWh = 0.0 - if previousTotalConsumptionWh ~= nil and previousTotalConsumptionWh.energy ~= nil then - deltaEnergyWh = math.max(latestTotalConsumptionWH - previousTotalConsumptionWh.energy, 0.0) - end - - local startTime = iso8061Timestamp(last_time) - local endTime = iso8061Timestamp(current_time - 1) + device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = latestTotalConsumptionWH - })) - end, "polling_report_schedule_timer") + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end - device:set_field(RECURRING_REPORT_POLL_TIMER, timer) + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + local component = device.profile.components["main"] + device:emit_component_event(component, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end @@ -263,7 +251,6 @@ local function device_init(driver, device) device:subscribe() create_poll_schedule(device) - create_poll_report_schedule(device) end end @@ -288,31 +275,18 @@ local function handle_refresh(self, device) end local function handle_resetEnergyMeter(self, device) - -- 978307200 is the number of seconds from 1 January 1970 to 1 January 2001 local current_time = os.time() + + -- 978307200 is the number of seconds from 1 January 1970 to 1 January 2001 local current_time_2001 = current_time - 978307200 if current_time_2001 < 0 then current_time_2001 = 0 end - local last_time = device:get_field(LAST_REPORT_TIME) or 0 - local startTime = iso8061Timestamp(last_time) - local endTime = iso8061Timestamp(current_time - 1) - -- Reset the consumption on the device local data = data_types.validate_or_build_type(current_time_2001, data_types.Uint32) device:send(cluster_base.write(device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, data)) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = 0, - energy = 0 - })) - - device:set_field(LAST_REPORT_TIME, current_time, { persist = true }) end ------------------------------------------------------------------------------------- @@ -365,6 +339,7 @@ local function watt_accumulated_attr_handler(driver, device, ib, zb_rx) local totalConsumptionRawValue = ib.data.value local totalConsumptionWh = utils.round(1000 * totalConsumptionRawValue) updateEnergyMeter(device, totalConsumptionWh) + report_power_consumption_to_st_energy(device, totalConsumptionWh) end 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 29544d0e8d..27fe47f11a 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 @@ -283,6 +283,8 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) ) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -294,8 +296,15 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) ) - -- in order to do powerConsumptionReport, CumulativeEnergyImported must be called twice. - -- This is because related variable settings are required in set_poll_report_timer_and_schedule(). + test.socket.capability:__expect_send( + aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -307,6 +316,11 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) + -- don't send a powerConsumptionReport event, 15 minutes have not passed since the last one. + + test.wait_for_events() + test.mock_time.advance_time(1500) + test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -320,12 +334,10 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) ) - -- to test powerConsumptionReport - test.mock_time.advance_time(2000) test.socket.capability:__expect_send( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", deltaEnergy = 0.0, energy = 39.0 })) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 30e3510e77..d981af3110 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -146,12 +146,11 @@ local function test_init() end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.mock_device.add_test_device(mock_device) - -- to test powerConsumptionReport - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") end test.set_test_init_function(test_init) local function test_init_periodic() + test.mock_device.add_test_device(mock_device_periodic) local subscribe_request = subscribed_attributes_periodic[1]:subscribe(mock_device_periodic) for i, cluster in ipairs(subscribed_attributes_periodic) do if i > 1 then @@ -159,9 +158,10 @@ local function test_init_periodic() end end test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_periodic) - -- to test powerConsumptionReport - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end test.register_message_test( @@ -254,6 +254,9 @@ test.register_message_test( test.register_coroutine_test( "Cumulative Energy measurement should generate correct messages", function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( { mock_device.id, @@ -262,20 +265,20 @@ test.register_coroutine_test( ) } ) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) ) - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) + test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) ) + test.socket.matter:__queue_receive( { mock_device.id, @@ -287,6 +290,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) ) + + test.wait_for_events() + test.mock_time.advance_time(1500) + test.socket.matter:__queue_receive( { mock_device.id, @@ -298,12 +305,11 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) ) - test.mock_time.advance_time(2000) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, energy = 39.0 })) ) @@ -335,6 +341,7 @@ test.register_message_test( test.register_coroutine_test( "Periodic Energy measurement should generate correct messages", function() + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive( { mock_device_periodic.id, @@ -346,6 +353,14 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 23.0 + })) + ) test.socket.matter:__queue_receive( { mock_device_periodic.id, @@ -357,6 +372,8 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) ) + test.wait_for_events() + test.mock_time.advance_time(2000) test.socket.matter:__queue_receive( { mock_device_periodic.id, @@ -368,12 +385,11 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 69.0, unit="Wh"})) ) - test.mock_time.advance_time(2000) test.socket.capability:__expect_send( mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z", + deltaEnergy = 46.0, energy = 69.0 })) ) @@ -381,275 +397,6 @@ test.register_coroutine_test( { test_init = test_init_periodic } ) -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds - -test.register_coroutine_test( - "Generated poll timer (<15 minutes) gets correctly set", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(899) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "import_timer should min_interval") - end -) - -test.register_coroutine_test( - "Generated poll timer (>15 minutes) gets correctly set", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 29.0 - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - end -) - -test.register_coroutine_test( - "Check when the device is removed", function() - - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_19 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - mock_device, 1, cumulative_report_val_29 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:33:19Z", - deltaEnergy = 0.0, - energy = 29.0 - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) - test.wait_for_events() - report_import_poll_timer = mock_device:get_field("__recurring_import_report_poll_timer") - import_timer_length = mock_device:get_field("__import_report_timeout") - assert(report_import_poll_timer == nil, "report_import_poll_timer should exist") - assert(import_timer_length == nil, "import_timer_length should exist") - end -) - -test.register_coroutine_test( - "Generated periodic import energy device poll timer (<15 minutes) gets correctly set", function() - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(899) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device_periodic:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device_periodic:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == MINIMUM_ST_ENERGY_REPORT_INTERVAL, "import_timer should min_interval") - end, - { test_init = test_init_periodic } -) - -test.register_coroutine_test( - "Generated periodic import energy device poll timer (>15 minutes) gets correctly set", function() - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 23.0, unit = "Wh" })) - ) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 46.0, unit = "Wh" })) - ) - test.wait_for_events() - test.mock_time.advance_time(2000) - test.socket["matter"]:__queue_receive( - { - mock_device_periodic.id, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data( - mock_device_periodic, 1, periodic_report_val_23 - ) - } - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 69.0, unit = "Wh" })) - ) - test.socket["capability"]:__expect_send( - mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - deltaEnergy=0.0, - ["end"] = "1970-01-01T00:33:19Z", - energy=69.0, - start="1970-01-01T00:00:00Z" - })) - ) - test.wait_for_events() - local report_import_poll_timer = mock_device_periodic:get_field("__recurring_import_report_poll_timer") - local import_timer_length = mock_device_periodic:get_field("__import_report_timeout") - assert(report_import_poll_timer ~= nil, "report_import_poll_timer should exist") - assert(import_timer_length ~= nil, "import_timer_length should exist") - assert(import_timer_length == 2000, "import_timer should min_interval") - end, - { test_init = test_init_periodic } -) - test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() 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 991593953e..5ac4cea964 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -164,9 +164,6 @@ test.register_coroutine_test( local poll_timer = mock_device:get_field("RECURRING_POLL_TIMER") assert(poll_timer ~= nil, "poll_timer should exist") - local report_poll_timer = mock_device:get_field("RECURRING_REPORT_POLL_TIMER") - assert(report_poll_timer ~= nil, "report_poll_timer should exist") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "removed" }) test.wait_for_events() end @@ -291,6 +288,15 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 50000, unit = "Wh" })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T16:39:59Z", + deltaEnergy = 0.0, + energy = 50000, + })) + ) + test.wait_for_events() end ) @@ -316,16 +322,6 @@ test.register_coroutine_test( test.socket.matter:__expect_send({ mock_device.id, cluster_base.write(mock_device, 0x01, PRIVATE_CLUSTER_ID, PRIVATE_ATTR_ID_ACCUMULATED_CONTROL_POINT, nil, data) }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 0, - deltaEnergy = 0, - start = "1970-01-01T00:00:00Z", - ["end"] = "2001-01-01T00:00:00Z" - })) - ) - test.wait_for_events() end ) @@ -368,23 +364,6 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0, unit = "W" })) ) - - test.wait_for_events() - -- after 15 minutes, the device should still report power consumption even when off - test.mock_time.advance_time(60 * 15) -- Ensure that the timer created in create_poll_schedule triggers - - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 0, - deltaEnergy = 0.0, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.wait_for_events() end, { test_init = function() diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 411c4c2104..2244eab661 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -123,14 +123,9 @@ SwitchFields.child_device_profile_overrides_per_vendor_id = { } SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -SwitchFields.FIRST_IMPORT_REPORT_TIMESTAMP = "__first_import_report_timestamp" -SwitchFields.IMPORT_POLL_TIMER_SETTING_ATTEMPTED = "__import_poll_timer_setting_attempted" -SwitchFields.IMPORT_REPORT_TIMEOUT = "__import_report_timeout" SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" -SwitchFields.RECURRING_IMPORT_REPORT_POLL_TIMER = "__recurring_import_report_poll_timer" SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds -SwitchFields.SUBSCRIPTION_REPORT_OCCURRED = "__subscription_report_occurred" SwitchFields.START_BUTTON_PRESS = "__start_button_press" SwitchFields.TIMEOUT_THRESHOLD = 10 --arbitrary timeout diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index e3f9e667f9..86368e9208 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -182,7 +182,6 @@ function utils.create_multi_press_values_list(size, supportsHeld) return list end - function utils.detect_bridge(device) for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do @@ -203,4 +202,43 @@ function utils.detect_matter_thing(device) return device:supports_capability(capabilities.refresh) end +function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + else + device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) + end +end + return utils diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 1e2c3422f3..3a03d0a0fd 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -102,16 +102,12 @@ local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 local MIN_ALLOWED_PERCENT_VALUE = 0 local MAX_ALLOWED_PERCENT_VALUE = 100 -local DEFAULT_REPORT_TIME_INTERVAL = 15 * 60 -- Report cumulative energy every 15 minutes -local MAX_REPORT_TIMEOUT = 30 * 60 -local POLL_INTERVAL = 60 -- To read CumulativeEnergyImported every 60 seconds. - -local RECURRING_POLL_TIMER = "__recurring_poll_timer" -local RECURRING_REPORT_TIMER = "__recurring_report_poll_timer" -local DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL = "__pcr_time_interval" -local DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED = "__timer_interval_considered" + +local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds + local TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" -local LAST_REPORTED_TIME = "__last_reported_time" local SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local MGM3_PPM_CONVERSION_FACTOR = 24.45 @@ -308,10 +304,12 @@ local subscribed_attributes = { clusters.WaterHeaterMode.attributes.SupportedModes }, [capabilities.powerConsumptionReport.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported }, [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported }, } @@ -347,60 +345,36 @@ local get_total_cumulative_energy_imported = function(device) return total_energy end -local function schedule_energy_report_timer(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end +local function report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - local polling_schedule_timer = device:get_field(RECURRING_REPORT_TIMER) - if polling_schedule_timer ~= nil then + -- Ensure that the previous report was sent at least 15 minutes ago + if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - -- The powerConsumption report needs to be updated at least every 15 minutes in order to be included in SmartThings Energy - local pcr_interval = device:get_field(DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL) or DEFAULT_REPORT_TIME_INTERVAL - pcr_interval = utils.clamp_value(pcr_interval, DEFAULT_REPORT_TIME_INTERVAL, MAX_REPORT_TIMEOUT) - local timer = device.thread:call_on_schedule(pcr_interval, function() - local last_time = device:get_field(LAST_REPORTED_TIME) or 0 - local current_time = os.time() - local total_energy = get_total_cumulative_energy_imported(device) - device:set_field(LAST_REPORTED_TIME, current_time, { persist = true }) - - -- Calculate the energy consumed between the start and the end time - local previousTotalConsumptionWh = device:get_latest_state( - "main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME - ) or { energy = 0 } - local deltaEnergyWh = math.max(total_energy - previousTotalConsumptionWh.energy, 0.0) - local startTime = epoch_to_iso8601(last_time) - local endTime = epoch_to_iso8601(current_time - 1) - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = startTime, - ["end"] = endTime, - deltaEnergy = deltaEnergyWh, - energy = total_energy - })) - end, "polling_report_schedule_timer") + device:set_field(LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - device:set_field(RECURRING_REPORT_TIMER, timer) -end - -local function delete_reporting_timer(device) - local reporting_poll_timer = device:get_field(RECURRING_REPORT_TIMER) - if reporting_poll_timer ~= nil then - device.thread:cancel_timer(reporting_poll_timer) - device:set_field(RECURRING_REPORT_TIMER, nil) + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) end local function device_removed(driver, device) - delete_reporting_timer(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - device.thread:cancel_timer(poll_timer) - device:set_field(RECURRING_POLL_TIMER, nil) - end + device.log.info("device removed") end local function tbl_contains(array, value) @@ -456,40 +430,6 @@ local endpoint_to_component = function (device, endpoint_id) return "main" end -local function create_poll_schedule(device) - local poll_timer = device:get_field(RECURRING_POLL_TIMER) - if poll_timer ~= nil then - return - end - - local cumul_imp_eps = embedded_cluster_utils.get_endpoints( - device, clusters.ElectricalEnergyMeasurement.ID, - { - feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY | - clusters.ElectricalEnergyMeasurement.types.Feature.IMPORTED_ENERGY - } - ) or {} - if #cumul_imp_eps == 0 then - return - end - - device:send(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)) - -- Setup a timer to read cumulative energy imported attribute every minute. - local timer = device.thread:call_on_schedule(POLL_INTERVAL, function() - device:send(clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(device)) - end, "polling_schedule_timer") - - device:set_field(RECURRING_POLL_TIMER, timer) -end - -local function schedule_polls_for_cumulative_energy_imported(device) - if not device:supports_capability(capabilities.powerConsumptionReport) then - return - end - create_poll_schedule(device) - schedule_energy_report_timer(device) -end - local function device_init(driver, device) if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then if version.api >= 15 and version.rpc >= 9 then @@ -513,7 +453,18 @@ local function device_init(driver, device) device:send(deadband_read) end end - schedule_polls_for_cumulative_energy_imported(device) + + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + end end local function info_changed(driver, device, event, args) @@ -526,7 +477,6 @@ local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then device:subscribe() end - schedule_polls_for_cumulative_energy_imported(device) end local function get_endpoints_for_dt(device, device_type) @@ -550,12 +500,12 @@ local function get_device_type(device) -- devices with similar device type compositions that may report their device types -- listed in different orders local device_type_priority = { - [RAC_DEVICE_TYPE_ID] = 1, - [AP_DEVICE_TYPE_ID] = 2, - [THERMOSTAT_DEVICE_TYPE_ID] = 3, - [FAN_DEVICE_TYPE_ID] = 4, - [WATER_HEATER_DEVICE_TYPE_ID] = 5, - [HEAT_PUMP_DEVICE_TYPE_ID] = 6 + [HEAT_PUMP_DEVICE_TYPE_ID] = 1, + [RAC_DEVICE_TYPE_ID] = 2, + [AP_DEVICE_TYPE_ID] = 3, + [THERMOSTAT_DEVICE_TYPE_ID] = 4, + [FAN_DEVICE_TYPE_ID] = 5, + [WATER_HEATER_DEVICE_TYPE_ID] = 6, } local main_device_type = false @@ -1988,42 +1938,19 @@ local function active_power_handler(driver, device, ib, response) end local function periodic_energy_imported_handler(driver, device, ib, response) - local endpoint_id = ib.endpoint_id - local cumul_eps = embedded_cluster_utils.get_endpoints(device, - clusters.ElectricalEnergyMeasurement.ID, - { feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY }) - if ib.data then if version.api < 11 then clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) end - - local start_timestamp = ib.data.elements.start_timestamp.value or 0 - local end_timestamp = ib.data.elements.end_timestamp.value or 0 - - local device_reporting_time_interval = end_timestamp - start_timestamp - if not device:get_field(DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED) and device_reporting_time_interval > DEFAULT_REPORT_TIME_INTERVAL then - -- This is a one time setup in order to consider a larger time interval if the interval the device chooses to report is greater than 15 minutes. - device:set_field(DEVICE_REPORTING_TIME_INTERVAL_CONSIDERED, true, { persist = true }) - device:set_field(DEVICE_POWER_CONSUMPTION_REPORT_TIME_INTERVAL, device_reporting_time_interval, { persist = true }) - delete_reporting_timer(device) - schedule_energy_report_timer(device) - end - - if tbl_contains(cumul_eps, endpoint_id) then - -- Since cluster at this endpoint supports both CUME & PERE features, we will prefer - -- cumulative_energy_imported_handler to handle the energy report for this endpoint. - return - end - - endpoint_id = string.format(ib.endpoint_id) + local endpoint_id = string.format(ib.endpoint_id) local energy_imported_Wh = utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) + device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) + report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) end end @@ -2038,7 +1965,18 @@ local function cumulative_energy_imported_handler(driver, device, ib, response) cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_event(capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) + device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) + report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +local function energy_report_handler_factory(is_cumulative_report) + return function(driver, device, ib, response) + if is_cumulative_report then + cumulative_energy_imported_handler(driver, device, ib, response) + elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then + periodic_energy_imported_handler(driver, device, ib, response) + end end end @@ -2225,8 +2163,8 @@ local matter_driver_template = { [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = cumulative_energy_imported_handler, - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = periodic_energy_imported_handler + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), }, [clusters.WaterHeaterMode.ID] = { [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = water_heater_mode_handler, 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 9af8c51982..b7d4b3a24a 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 @@ -101,6 +101,7 @@ local test_init_common = function(device) clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(device) @@ -132,9 +133,6 @@ end local mock_device = test.mock_device.build_test_matter_device(device_desc) local function test_init() test_init_common(mock_device) - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) end -- Create device with Thermostat clusters having features AUTO, HEAT & COOL @@ -143,9 +141,6 @@ device_desc.endpoints[4].clusters[1].feature_map = 35 local mock_device_with_auto = test.mock_device.build_test_matter_device(device_desc) local test_init_auto = function() test_init_common(mock_device_with_auto) - test.socket.matter:__expect_send({ - mock_device_with_auto.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device_with_auto) - }) test.socket.matter:__expect_send({ mock_device_with_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_with_auto) }) @@ -153,13 +148,6 @@ end -- Set feature map of ElectricalEnergyMeasurement Cluster to only PERE and IMPE device_desc.endpoints[2].clusters[1].feature_map = 9 -local mock_device_with_pere_impe = test.mock_device.build_test_matter_device(device_desc) -local test_init_pere_impe = function() - test_init_common(mock_device_with_pere_impe) - test.socket.matter:__expect_send({ - mock_device_with_pere_impe.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_with_pere_impe) - }) -end test.set_test_init_function(test_init) @@ -657,12 +645,9 @@ test.register_message_test( ) test.register_coroutine_test( - "The total energy consumption of the device must be reported every 15 minutes", + "The total energy consumption of the device must be reported a maximum of every 15 minutes", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, @@ -675,24 +660,18 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 20, - deltaEnergy = 20, + deltaEnergy = 0.0, start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" + ["end"] = "1970-01-01T00:15:00Z" })) - ) + ) test.wait_for_events() - - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(60 * 10) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, @@ -704,60 +683,10 @@ test.register_coroutine_test( value = 30, unit = "Wh" })) ) - - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 30, - deltaEnergy = 10, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" - })) - ) - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Ensure the driver does not send read request to devices without CUME & IMPE features", - function() - local timer = mock_device_with_pere_impe:get_field("__recurring_poll_timer") - assert(timer == nil, "Polling timer must not be created if the device does not support CUME & IMPE features") - end, - { - test_init = function() - test_init_pere_impe() - end - } -) - -test.register_coroutine_test( - "PeriodicEnergyImported should report the energyMeter values", - function() - test.socket.matter:__queue_receive({ mock_device_with_pere_impe.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device_with_pere_impe, - HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 100, start_systime = 0, end_systime = 0 })) }) -- 30Wh - - test.socket.capability:__expect_send( - mock_device_with_pere_impe:generate_test_message("main", - capabilities.energyMeter.energy({ - value = 30, unit = "Wh" - })) - ) - end, - { - test_init = function() - test_init_pere_impe() end } ) @@ -765,10 +694,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Ensure only the cumulative energy reports are considered if the device supports both PERE and CUME features.", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, @@ -781,6 +707,16 @@ test.register_coroutine_test( })) ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:18:00Z", + deltaEnergy = 0.0, + energy = 20 + })) + ) + + test.mock_time.advance_time(180) -- move time 180s, which is less than 15m (obviously). test.wait_for_events() -- do not expect energyMeter event for this report. @@ -788,78 +724,21 @@ test.register_coroutine_test( HEAT_PUMP_EP, clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 800, start_systime = 0, end_systime = 0 })) }) -- 20Wh - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.wait_for_events() - end, - { - test_init = function() - test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") - end - } -) - -test.register_coroutine_test( - "Consider the device reported time interval in case it is greater than 15 minutes for powerConsumptionReport capability reports", - function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) - + -- do not expect a powerConsumptionReport to be emitted test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 50Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.energyMeter.energy({ - value = 20, unit = "Wh" + value = 50, unit = "Wh" })) ) - - test.wait_for_events() - - -- do not expect energyMeter event for this report. Only consider the time interval as it is greater than 15 minutes. - test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, - HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 1080, start_systime = 0, end_systime = 0 })) }) -- 20Wh 18 minutes - - - test.wait_for_events() - test.mock_time.advance_time(60 * 18) - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.powerConsumptionReport.powerConsumption({ - energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:17:59Z" - })) - ) - - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60 * 18, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) 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 0dbeff9796..802f323181 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 @@ -88,7 +88,8 @@ local function test_init() clusters.WaterHeaterMode.attributes.CurrentMode, clusters.WaterHeaterMode.attributes.SupportedModes, clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) @@ -99,9 +100,6 @@ local function test_init() end test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.mock_device.add_test_device(mock_device) - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) end test.set_test_init_function(test_init) @@ -263,12 +261,9 @@ test.register_message_test( ) test.register_coroutine_test( - "The total energy consumption of the device must be reported every 15 minutes", + "The total energy consumption of the device must be reported, but in 15+ minute intervals", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + test.mock_time.advance_time(901) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, @@ -281,24 +276,15 @@ test.register_coroutine_test( })) ) - test.wait_for_events() - test.mock_time.advance_time(60 * 15) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ energy = 20, - deltaEnergy = 20, - start = "1970-01-01T00:00:00Z", - ["end"] = "1970-01-01T00:14:59Z" - })) - ) - - test.wait_for_events() - - test.socket.matter:__expect_send({ - mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:read(mock_device) - }) + deltaEnergy = 0.0, + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z" + })) + ) test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, @@ -312,24 +298,33 @@ test.register_coroutine_test( ) test.wait_for_events() - test.mock_time.advance_time(60 * 15) + test.mock_time.advance_time(1001) + + + test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, + ELECTRICAL_SENSOR_EP, + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ + value = 50, unit = "Wh" + })) + ) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ - energy = 30, - deltaEnergy = 10, - start = "1970-01-01T00:15:00Z", - ["end"] = "1970-01-01T00:29:59Z" + energy = 50, + deltaEnergy = 30, + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:31:41Z" })) ) - test.wait_for_events() end, { test_init = function() test_init() - test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "polling_report_schedule_timer") - test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end } ) From 47bf57b0c7fd12c6b553facbf69c09b28fbb17a6 Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Fri, 19 Sep 2025 18:07:44 +0800 Subject: [PATCH 168/449] This commit fixes the issue of some devices "false alarm" after hub switch-over. We add check_latest state before sending initial event(open/unlocked/presense) for following drivers: - SmartThings/zigbee-button/src/aqara/init.lua - SmartThings/zigbee-button/src/dimming-remote/init.lua - SmartThings/zigbee-button/src/frient/init.lua - SmartThings/zigbee-button/src/init.lua - SmartThings/zigbee-button/src/iris/init.lua - SmartThings/zigbee-button/src/pushButton/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua - SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/init.lua - SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua - SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua - SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua - SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua - SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua - SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua - SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua - SmartThings/zigbee-window-treatment/src/aqara/init.lua - SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua - SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua - Unofficial/tuya-zigbee/src/button/init.lua - Unofficial/tuya-zigbee/src/button/meian-button/init.lua - Unofficial/tuya-zigbee/src/curtain/init.lua --- .../zigbee-button/src/aqara/init.lua | 3 +- .../zigbee-button/src/button_utils.lua | 6 +++ .../zigbee-button/src/dimming-remote/init.lua | 2 +- .../zigbee-button/src/frient/init.lua | 3 +- .../SmartThings/zigbee-button/src/init.lua | 3 +- .../zigbee-button/src/iris/init.lua | 2 +- .../zigbee-button/src/pushButton/init.lua | 3 +- .../src/test/test_aqara_button.lua | 12 +++++ .../src/test/test_dimming_remote.lua | 45 +++++++++++++++- .../src/test/test_frient_button.lua | 17 +++--- .../src/test/test_ikea_on_off.lua | 43 +++++++++++++++ .../src/test/test_ikea_open_close.lua | 42 +++++++++++++++ .../src/test/test_ikea_remote_control.lua | 52 ++++++++++++++++-- .../src/test/test_iris_button.lua | 21 +++++++- .../src/test/test_robb_4x_button.lua | 52 ++++++++++++++++++ .../src/test/test_robb_8x_button.lua | 53 +++++++++++++++++++ .../ikea/TRADFRI_remote_control.lua | 2 +- .../src/zigbee-multi-button/ikea/init.lua | 5 +- .../src/zigbee-multi-button/init.lua | 3 +- .../src/zigbee-multi-button/robb/init.lua | 3 +- .../src/zigbee-accessory-dimmer/init.lua | 13 +++-- .../zigbee-battery-accessory-dimmer/init.lua | 8 ++- .../src/aqara/multi-switch/init.lua | 3 +- .../src/inovelli-vzm31-sn/init.lua | 11 ++-- .../zigbee-switch/src/switch_utils.lua | 23 ++++++++ .../src/test/test_aqara_switch_no_power.lua | 19 ++++++- .../src/zigbee-dimming-light/init.lua | 3 +- .../src/aqara/curtain-driver-e1/init.lua | 5 +- .../src/aqara/init.lua | 5 +- .../src/aqara/roller-shade/init.lua | 5 +- .../test_zigbee_window_treatment_aqara.lua | 32 +++++++++++ ...ndow_treatment_aqara_curtain_driver_e1.lua | 19 +++++++ ...ow_treatment_aqara_roller_shade_rotate.lua | 23 ++++++++ .../src/window_treatment_utils.lua | 23 ++++++++ .../src/eaton-anyplace-switch/init.lua | 4 +- .../zwave-switch/src/switch_utils.lua | 23 ++++++++ .../src/test/test_eaton_anyplace_switch.lua | 34 ++++++++++++ .../tuya-zigbee/src/button/init.lua | 3 +- .../src/button/meian-button/init.lua | 3 +- .../tuya-zigbee/src/curtain/init.lua | 4 +- .../src/test/test_meian_button.lua | 21 ++++++++ .../tuya-zigbee/src/test/test_tuya_button.lua | 15 ++++++ .../src/test/test_tuya_curtain.lua | 10 ++++ .../Unofficial/tuya-zigbee/src/tuya_utils.lua | 6 +++ 44 files changed, 640 insertions(+), 47 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/switch_utils.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua create mode 100644 drivers/SmartThings/zwave-switch/src/switch_utils.lua diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 5991fd4cb5..174d775324 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -17,6 +17,7 @@ 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 button_utils = require "button_utils" local PowerConfiguration = clusters.PowerConfiguration @@ -83,7 +84,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:emit_event(capabilities.battery.battery(100)) end diff --git a/drivers/SmartThings/zigbee-button/src/button_utils.lua b/drivers/SmartThings/zigbee-button/src/button_utils.lua index b7499684cd..4a8f7101e0 100644 --- a/drivers/SmartThings/zigbee-button/src/button_utils.lua +++ b/drivers/SmartThings/zigbee-button/src/button_utils.lua @@ -79,4 +79,10 @@ button_utils.build_button_handler = function(button_name, pressed_type) end end +button_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + return button_utils diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua index 45809d728c..d08c975632 100644 --- a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua @@ -54,7 +54,7 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons}, {visibility = { displayed = false }})) end device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function do_configure(self, device) diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index e7df0e61c6..17b963548d 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -17,6 +17,7 @@ local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local data_types = require "st.zigbee.data_types" +local button_utils = require "button_utils" local BasicInput = zcl_clusters.BasicInput local PowerConfiguration = zcl_clusters.PowerConfiguration local OnOff = zcl_clusters.OnOff @@ -163,7 +164,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local function do_configure(driver, device, event, args) diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index 776eaa8b9e..a61ff415a7 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -18,6 +18,7 @@ local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" local IASZone = (require "st.zigbee.zcl.clusters").IASZone local TemperatureMeasurement = (require "st.zigbee.zcl.clusters").TemperatureMeasurement +local button_utils = require "button_utils" local temperature_measurement_defaults = { MIN_TEMP = "MIN_TEMP", @@ -109,7 +110,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) if device:supports_server_cluster(TemperatureMeasurement.ID) then device:send(TemperatureMeasurement.attributes.MaxMeasuredValue:read(device)) device:send(TemperatureMeasurement.attributes.MinMeasuredValue:read(device)) diff --git a/drivers/SmartThings/zigbee-button/src/iris/init.lua b/drivers/SmartThings/zigbee-button/src/iris/init.lua index 1baa2f8ced..157e766748 100644 --- a/drivers/SmartThings/zigbee-button/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-button/src/iris/init.lua @@ -62,7 +62,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) end diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua index 8d09684fd7..6fec59d69e 100644 --- a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua +++ b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua @@ -24,11 +24,12 @@ -- for the specific language governing permissions and limitations under the License. local capabilities = require "st.capabilities" +local button_utils = require "button_utils" local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local push_button = { 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 ba0f87741f..b53a966761 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -67,22 +67,34 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle -- e1", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) end ) test.register_coroutine_test( "Handle added lifecycle -- t1", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) end ) 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 32e686a9f5..4ba87cb461 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua @@ -176,6 +176,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -221,7 +222,49 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + 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( + "button1", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 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 ef20adc678..3df15b9f08 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -165,18 +165,23 @@ test.register_coroutine_test( [15] = 0, [10] = 0 } + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) 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.numberOfButtons({value = 1}))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({ state_change = false}))) test.wait_for_events() - - for voltage, batt_perc in pairs(battery_table) do - test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc)) ) - - end + for voltage, batt_perc in pairs(battery_table) do + test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, voltage) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc))) + end + test.wait_for_events() + -- Avoid sending the button pushed contactSensor 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.supportedButtonValues({"pushed"}, {visibility = { displayed = false }}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) + test.wait_for_events() end ) 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 f685d6fbec..9479358793 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 @@ -211,6 +211,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -251,6 +252,48 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed 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.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed", "held" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ 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 ec5cb8a1dc..7b41684c5a 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 @@ -155,6 +155,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send({ mock_device.id, @@ -195,6 +196,47 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) + -- Avoid sending the initial button pushed 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.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = "main", + attribute_id = "numberOfButtons", state = { value = 2 } + } + }) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "supportedButtonValues", state = { value = { "pushed" } } + } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "button", component_id = button_name, + attribute_id = "numberOfButtons", state = { value = 1 } + } + }) + end + end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.zigbee:__expect_send({ 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 dade3489c5..6b16ed842c 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 @@ -188,6 +188,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -230,14 +231,59 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - - test.socket.capability:__expect_send({ + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.capability:__expect_send({ mock_device.id, { capability_id = "button", component_id = "main", attribute_id = "button", state = { value = "pushed" } } }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 5 }, { visibility = { displayed = false } }) + ) + ) + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" then + if button_name ~= "button5" then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed"}, { visibility = { displayed = false } }) + ) + ) + end + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.wait_for_events() end @@ -259,4 +305,4 @@ test.register_message_test( } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 032b9c18d1..f28e47dece 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua @@ -129,6 +129,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -150,7 +151,25 @@ test.register_coroutine_test( attribute_id = "button", state = { value = "pushed" } } }) - + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "held" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) 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 733e79e304..554decefc1 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 @@ -218,6 +218,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -271,6 +272,57 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 4 }, { visibility = { displayed = false } }) + ) + ) + end + end + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) 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 bbac26d61c..67554e92a0 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 @@ -335,6 +335,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") @@ -388,6 +389,58 @@ test.register_coroutine_test( mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + -- Avoid sending the initial button pushed 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:__set_channel_ordering("relaxed") + + for button_name, _ in pairs(mock_device.profile.components) do + if button_name ~= "main" and (button_name == "button1" or button_name == "button3" or button_name == "button5" or button_name == "button7") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "up_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + elseif button_name ~= "main" and (button_name == "button2" or button_name == "button4" or button_name == "button6" or button_name == "button8") then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.supportedButtonValues({ "pushed", "down_hold" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + else + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "up_hold", "down_hold" }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 8 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) end ) diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua index 46cc01c4f9..0d9f12a697 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua @@ -58,7 +58,7 @@ local function added_handler(self, device) end end device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local remote_control = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua index f01f29715f..9a66a85991 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua @@ -22,6 +22,7 @@ local mgmt_bind_req = require "st.zigbee.zdo.mgmt_bind_request" local utils = require 'st.utils' local zdo_messages = require "st.zigbee.zdo" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration @@ -80,8 +81,8 @@ local function added_handler(self, device) device:emit_component_event(component, capabilities.button.numberOfButtons({value = number_of_buttons})) end device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) - device:emit_event(capabilities.button.button.pushed({state_change = false})) -end + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) + end local function zdo_binding_table_handler(driver, device, zb_rx) for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 369b3aaaf9..539b03785b 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -14,6 +14,7 @@ local capabilities = require "st.capabilities" local supported_values = require "zigbee-multi-button.supported_values" +local button_utils = require "button_utils" local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { { mfr = "CentraLite", model = "3450-L" }, @@ -78,7 +79,7 @@ local function added_handler(self, device) capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) end end - device:emit_event(capabilities.button.button.pushed({state_change = false})) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local zigbee_multi_button = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua index 8775e27bdc..dddef6f595 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua @@ -6,6 +6,7 @@ local Level = zcl_clusters.Level local OnOff = zcl_clusters.OnOff local PowerConfiguration = zcl_clusters.PowerConfiguration local capabilities = require "st.capabilities" +local button_utils = require "button_utils" --[[ The ROBB Wireless Remote Control has 4 or 8 buttons. They are arranged in two columns: @@ -130,7 +131,7 @@ local function added_handler(self, device) device:emit_component_event(comp, capabilities.button.numberOfButtons({ value = number_of_buttons }, { visibility = { displayed = false } })) end - device:emit_event(capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua index 3e02125863..91662454c9 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua @@ -122,11 +122,17 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, 100) + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) + end device:emit_event(capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } })) device:emit_event(capabilities.button.supportedButtonValues({"pushed", "held"}, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({state_change = true})) + if device:get_latest_state("main", capabilities.button.ID, capabilities.button.button.NAME) == nil then + device:emit_event(capabilities.button.button.pushed({state_change = true})) + end end local do_configure = function(self, device) @@ -142,7 +148,6 @@ local is_zigbee_accessory_dimmer = function(opts, driver, device) return true end end - return false end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua index c14e4263c3..134931c2c8 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua @@ -81,8 +81,12 @@ local switch_level_set_level_command_handler = function(driver, device, command) end local device_added = function(self, device) - generate_switch_onoff_event(device, "on") - generate_switch_level_event(device, DEFAULT_LEVEL) + if device:get_latest_state("main", capabilities.switch.ID, capabilities.switch.switch.NAME) == nil then + generate_switch_onoff_event(device, "on") + end + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil then + generate_switch_level_event(device, DEFAULT_LEVEL) + end end local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) 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 1c1431f518..4e54b67e2f 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -3,6 +3,7 @@ local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local configurations = require "configurations" +local switch_utils = require "switch_utils" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 @@ -89,7 +90,7 @@ local function device_added(driver, device) end device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) - device:emit_event(capabilities.button.button.pushed({ state_change = 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 local function device_init(self, device) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua index d6d094209c..4176e30113 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua @@ -20,6 +20,7 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" local configurations = require "configurations" +local switch_utils = require "switch_utils" local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" @@ -228,11 +229,11 @@ local device_init = function(self, device) end device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) else - device:emit_event(capabilities.colorControl.hue(1)) - device:emit_event(capabilities.colorControl.saturation(1)) - device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) - device:emit_event(capabilities.switchLevel.level(100)) - device:emit_event(capabilities.switch.switch("off")) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.hue.NAME, capabilities.colorControl.hue(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.saturation.NAME, capabilities.colorControl.saturation(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature(6500)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel.NAME, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch("off")) end end diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua new file mode 100644 index 0000000000..7f5429dceb --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils 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 d605f5480d..91683622d1 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 @@ -70,6 +70,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Lifecycle - added test", 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_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, @@ -80,13 +81,22 @@ test.register_coroutine_test( 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.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 } }))) end ) test.register_coroutine_test( "Lifecycle - added test", 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_child.id, "added" }) test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, @@ -94,6 +104,13 @@ 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 }))) + -- 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 } }))) end ) diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index e74f544ad0..28312a45e1 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -15,6 +15,7 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local configurations = require "configurations" +local switch_utils = require "switch_utils" local OnOff = clusters.OnOff local Level = clusters.Level @@ -91,7 +92,7 @@ local function device_init(driver, device) end local function device_added(driver, device) - device:emit_event(capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) end local zigbee_dimming_light = { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index bbbe24112e..5d4f4d3a37 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -16,6 +16,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Groups = clusters.Groups local Basic = clusters.Basic @@ -41,8 +42,8 @@ local SHADE_STATE_STOP = 2 local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) device:emit_event(hookLockState.hookLockState.unlocked()) device:emit_event(chargingState.chargingState.stopped()) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index 929d5b713d..ae9513734c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -3,6 +3,7 @@ local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering @@ -169,8 +170,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) device:emit_event(deviceInitialization.supportedInitializedState({ "notInitialized", "initializing", "initialized" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(deviceInitialization.initializedState.notInitialized()) device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 6239fb52f0..c7b6b9a50a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -4,6 +4,7 @@ local cluster_base = require "st.zigbee.cluster_base" local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" local data_types = require "st.zigbee.data_types" local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering @@ -93,8 +94,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) 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 37c6b501ed..aa286d24f7 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 @@ -81,6 +81,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -117,6 +118,37 @@ test.register_coroutine_test( cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, data_types.CharString, PREF_SOFT_TOUCH_ON) }) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "stse.deviceInitialization", component_id = "main", + attribute_id = "supportedInitializedState", + state = { value = { "notInitialized", "initializing", "initialized" } }, + visibility = { displayed = false } + } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", deviceInitialization.initializedState.notInitialized()) + ) + 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.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_SOFT_TOUCH_ON) }) end ) 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 f9a9785429..2b095c6c16 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 @@ -77,6 +77,7 @@ end test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -100,6 +101,24 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", hookLockState.hookLockState.unlocked()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", chargingState.chargingState.stopped()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(100)) + ) end ) 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 fd8153ab96..e241a8c81f 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 @@ -67,6 +67,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -85,6 +86,28 @@ test.register_coroutine_test( mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({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.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, MFG_CODE, + data_types.CharString, + PREF_REVERSE_OFF) }) + -- Avoid sending the initial window shade 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.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", shadeRotateState.rotateState.idle({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 , diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua new file mode 100644 index 0000000000..2f20ff2b4f --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 window_treatment_utils = {} + +window_treatment_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return window_treatment_utils diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua index 720f88b56f..b2110623f6 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua @@ -18,6 +18,8 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Basic local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +local switch_utils = require "switch_utils" + local EATON_ANYPLACE_SWITCH_FINGERPRINTS = { { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch } @@ -46,7 +48,7 @@ local function basic_get_handler(self, device, cmd) end local function device_added(driver, device) - device:emit_event(capabilities.switch.switch.off()) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch.off()) end local function switch_on_handler(driver, device) diff --git a/drivers/SmartThings/zwave-switch/src/switch_utils.lua b/drivers/SmartThings/zwave-switch/src/switch_utils.lua new file mode 100644 index 0000000000..2d0d2f4d89 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/switch_utils.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 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 switch_utils = {} + +switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + +return switch_utils \ No newline at end of file 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 93783aa1a4..1661c34f2c 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 @@ -45,6 +45,7 @@ test.set_test_init_function(test_init) test.register_message_test( "Basic SET 0x00 should be handled as switch off", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -60,6 +61,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.off()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0x00})) } + }, { channel = "capability", direction = "send", @@ -71,6 +88,7 @@ test.register_message_test( test.register_message_test( "Basic SET 0xFF should be handled as switch on", { + -- The initial switch event should be send during the device's first time onboarding { channel = "device_lifecycle", direction = "receive", @@ -86,6 +104,22 @@ test.register_message_test( direction = "receive", message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device.id, "added"} + }, + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({value=0xFF})) } + }, { channel = "capability", direction = "send", diff --git a/drivers/Unofficial/tuya-zigbee/src/button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/init.lua index a3df25a7f8..61599e3d5a 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/init.lua @@ -17,6 +17,7 @@ local clusters = require "st.zigbee.zcl.clusters" local OnOff = clusters.OnOff local device_management = require "st.zigbee.device_management" local PRESENT_ATTRIBUTE_ID = 0x00fd +local tuya_utils = require "tuya_utils" local FINGERPRINTS = { { mfr = "_TZ3000_ja5osu5g", model = "TS004F"}, @@ -36,7 +37,7 @@ end local function added_handler(self, device) device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end local tuya_private_cluster_button_handler = function(driver, device, zb_rx) diff --git a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua index f0281f91f5..887826a9f0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/button/meian-button/init.lua @@ -21,6 +21,7 @@ local data_types = require "st.zigbee.data_types" local messages = require "st.zigbee.messages" local defaults = require "st.zigbee.defaults" local PowerConfiguration = clusters.PowerConfiguration +local tuya_utils = require "tuya_utils" local IASACE = clusters.IASACE @@ -63,7 +64,7 @@ end local function added_handler(driver, device, event, args) device:emit_event(capabilities.button.supportedButtonValues({"pushed"}, {visibility = { displayed = false }})) device:emit_event(capabilities.button.numberOfButtons({value = 1}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.button.pushed({state_change = false})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) local magic_spell = {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe} device:send(read_attribute_function(device, clusters.Basic.ID, magic_spell)) diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 1eb2e94340..1d4d494815 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -43,8 +43,8 @@ end local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - device:emit_event(capabilities.windowShadeLevel.shadeLevel(0)) - device:emit_event(capabilities.windowShade.windowShade.closed()) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) end local function increase_packet_id(packet_id) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua index a97ff0af10..c3610f55ea 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_meian_button.lua @@ -136,6 +136,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should configure all necessary attributes", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( @@ -162,6 +163,26 @@ test.register_coroutine_test( mock_device_meian_button.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) }) + -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_device_meian_button.id, "added" }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device_meian_button:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.zigbee:__expect_send({ mock_device_meian_button.id, tuya_utils.build_tuya_magic_spell_message(mock_device_meian_button) }) + test.socket.zigbee:__expect_send({ + mock_device_meian_button.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_meian_button) + }) end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua index 431cbc2c48..73567c24e0 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_button.lua @@ -47,6 +47,7 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "added lifecycle event", function() + -- The initial button pushed event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( mock_device:generate_test_message( @@ -63,6 +64,20 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", button.button.pushed({ state_change = false })) ) + -- Avoid sending the initial button pushed 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.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) end ) 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 d6beda0de8..44f11a09f4 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -88,6 +88,7 @@ test.register_coroutine_test( test.register_coroutine_test( "added lifecycle event", function() + -- The initial window shade event should be send during the device's first time onboarding test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) test.socket.capability:__expect_send( mock_simple_device:generate_test_message( @@ -107,6 +108,15 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closed() ) ) + -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}}) + ) + ) + end ) diff --git a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua index 8e3c707b7d..f28a6a87c1 100644 --- a/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua +++ b/drivers/Unofficial/tuya-zigbee/src/tuya_utils.lua @@ -157,6 +157,12 @@ tuya_utils.build_tuya_magic_spell_message = function(device) }) end +tuya_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + tuya_utils.TUYA_PRIVATE_CLUSTER = TUYA_PRIVATE_CLUSTER tuya_utils.DP_TYPE_BOOL = DP_TYPE_BOOL tuya_utils.DP_TYPE_ENUM = DP_TYPE_ENUM From 925b0509f33884bb075cd0c44285fb25b87143d9 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 24 Sep 2025 11:19:11 -0700 Subject: [PATCH 169/449] WWSTCERT-8033 LIFX Everyday Smart Light 2-Pack (#2417) --- 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 a53d91cf16..f1c7b0f4cb 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -751,6 +751,11 @@ matterManufacturer: vendorId: 0x1423 productId: 0x0077 deviceProfileName: light-level-colorTemperature-1500k-9000k + - id: "5155/191" + deviceLabel: LIFX Everyday Smart Light 2-Pack + vendorId: 0x1423 + productId: 0x00BF + deviceProfileName: light-color-level #LG - id: "4142/8784" deviceLabel: LG Smart Button (1 Button) From 906dd4b576ab29d80bc9588da81a6cb5b35e9757 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 29 Sep 2025 14:56:02 -0700 Subject: [PATCH 170/449] WWSTCERT-8129 Heiman Smart water leakage sensor --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index b5ac21330d..9a463e7db6 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -69,6 +69,11 @@ matterManufacturer: vendorId: 0x120B productId: 0x1007 deviceProfileName: co + - id: "4619/4104" + deviceLabel: Smart water leakage sensor + vendorId: 0x120B + productId: 0x1008 + deviceProfileName: leak-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor From ed541a97d582d78d7f424b4da763dea62f93ae94 Mon Sep 17 00:00:00 2001 From: DevelcoProductsAS Date: Tue, 30 Sep 2025 09:03:50 +0200 Subject: [PATCH 171/449] Add support for WISZB-131 --- .../zigbee-contact/fingerprints.yml | 5 + .../frient-contact-battery-temperature.yml | 28 ++ .../zigbee-contact/src/configurations.lua | 1 + .../zigbee-contact/src/frient/init.lua | 2 +- .../test/test_frient_contact_sensor_2_pro.lua | 413 ++++++++++++++++++ 5 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml create mode 100644 drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index 172361da3b..f35ca2f7ba 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -149,6 +149,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: WISZB-121 deviceProfileName: contact-battery-profile + - id: "frient A/S/WISZB-131" + deviceLabel: frient Entry Sensor 2 Pro + manufacturer: frient A/S + model: WISZB-131 + deviceProfileName: frient-contact-battery-temperature - id: "Compacta/ZBWDS" deviceLabel: Smartenit Open/Closed Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml b/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml new file mode 100644 index 0000000000..6eea0a0c2d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/frient-contact-battery-temperature.yml @@ -0,0 +1,28 @@ +name: frient-contact-battery-temperature +components: +- id: main + capabilities: + - id: contactSensor + version: 1 + - id: battery + version: 1 + - id: temperatureMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°C)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index 6acaeb5e04..aa28e1e43f 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -92,6 +92,7 @@ local devices = { { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, { mfr = "frient A/S", model = "WISZB-120" }, + { mfr = "frient A/S", model = "WISZB-131" }, { mfr = "Compacta", model = "ZBWDS" } }, CONFIGURATION = { diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index 21f2c88c77..e732664d91 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -93,7 +93,7 @@ local frient_sensor = { } }, can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S" and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121")) + return (device:get_manufacturer() == "frient A/S" and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131")) end } 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 new file mode 100644 index 0000000000..12bfbc0b1e --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua @@ -0,0 +1,413 @@ +-- Copyright 2025 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 test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement + +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 + +local base64 = require "base64" +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("frient-contact-battery-temperature.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-131", + server_clusters = { 0x0005, 0x0006 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + } + } + } +) + +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_coroutine_test( + "Refresh necessary 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, IASZone.attributes.ZoneStatus:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MinMeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MaxMeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 1800, + 100 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 0 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(IASZONE_ENDPOINT) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +-- test.register_coroutine_test( +-- "Health check should check all relevant attributes", +-- function() +-- test.wait_for_events() + +-- test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval +-- test.socket.zigbee:__set_channel_ordering("relaxed") + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- PowerConfiguration.attributes.BatteryVoltage:read(mock_device) +-- } +-- ) + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) +-- } +-- ) + +-- test.socket.zigbee:__expect_send( +-- { +-- mock_device.id, +-- IASZone.attributes.ZoneStatus:read(mock_device) +-- } +-- ) +-- end, +-- { +-- test_init = function() +-- test.mock_device.add_test_device(mock_device) +-- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") +-- end +-- } +-- ) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0005) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "Reported ZoneStatus should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0004) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 1800, + temperatureSensitivity + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + end +) + +test.run_registered_tests() From d85afffb055a81890ebe03aa9d1e72f947ec4304 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 30 Sep 2025 10:43:51 -0700 Subject: [PATCH 172/449] WWSTCERT-8141 Decora Smart Wi-Fi ELV Dimmer WWSTCERT-8144 Decora Smart Wi-Fi 0-10V Dimmer WWSTCERT-8148 Decora Smart Wi-Fi (2nd Gen) Mini Plug-In Dimmer WWSTCERT-8151 Decora Smart Wi-Fi (2nd Gen) Tamper Resistant Outlet WWSTCERT-8154 Decora Smart Wi-Fi Mini Plug-In Outlet WWSTCERT-8157 Decora Smart Wi-Fi Outdoor Plug-In Switch WWSTCERT-8160 Decora Evolve Smart Switch Module WWSTCERT-8163 Decora Evolve Smart Dimmer Module --- .../matter-switch/fingerprints.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index f1c7b0f4cb..0b62c75b11 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -603,6 +603,46 @@ matterManufacturer: vendorId: 0x109B productId: 0x1000 deviceProfileName: switch-level + - id: "4251/4107" + deviceLabel: Decora Smart Wi-Fi ELV Dimmer + vendorId: 0x109B + productId: 0x100B + deviceProfileName: switch-level + - id: "4251/4110" + deviceLabel: Decora Smart Wi-Fi 0-10V Dimmer + vendorId: 0x109B + productId: 0x100E + deviceProfileName: switch-level + - id: "4251/4108" + deviceLabel: Decora Evolve Smart Dimmer Module + vendorId: 0x109B + productId: 0x100C + deviceProfileName: switch-level + - id: "4251/4109" + deviceLabel: Decora Evolve Smart Switch Module + vendorId: 0x109B + productId: 0x100D + deviceProfileName: switch-binary + - id: "4251/4105" + deviceLabel: Decora Smart Wi-Fi Outdoor Plug-In Switch + vendorId: 0x109B + productId: 0x1009 + deviceProfileName: switch-binary + - id: "4251/4099" + deviceLabel: Leviton Decora Smart Wi-Fi Mini Plug-In Outlet + vendorId: 0x109B + productId: 0x1003 + deviceProfileName: switch-binary + - id: "4251/4100" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Tamper Resistant Outlet + vendorId: 0x109B + productId: 0x1004 + deviceProfileName: switch-binary + - id: "4251/4098" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Mini Plug-In Dimmer + vendorId: 0x109B + productId: 0x1002 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From 7e7c9cc9896d966afe22bdc69b587b65ee369179 Mon Sep 17 00:00:00 2001 From: Bang Keuckdo Date: Wed, 1 Oct 2025 18:24:50 +0900 Subject: [PATCH 173/449] Add test case to enhance code coverage for zigbee switch. Signed-off-by: Bang Keuckdo --- .../src/inovelli-vzm31-sn/init.lua | 10 +- .../src/test/test_aqara_switch_no_power.lua | 42 ++ .../test/test_enbrighten_metering_dimmer.lua | 104 +++- .../src/test/test_inovelli-vzm31-sn.lua | 484 ++++++++++++++++++ .../src/test/test_jasco_switch.lua | 2 +- .../test/test_robb_smarrt_2-wire_dimmer.lua | 2 +- .../src/test/test_robb_smarrt_knob_dimmer.lua | 2 +- .../src/test/test_switch_power.lua | 159 ++++++ .../src/test/test_tuya_multi.lua | 93 ++++ .../test/test_zigbee_dimmer_power_energy.lua | 190 ------- .../enbrighten-metering-dimmer/init.lua | 67 --- .../src/zigbee-dimmer-power-energy/init.lua | 31 +- .../zigbee-switch/src/zll-polling/init.lua | 2 +- 13 files changed, 911 insertions(+), 277 deletions(-) create mode 100755 drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua create mode 100755 drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua delete mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua delete mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua index 4176e30113..a0dae6db3f 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua @@ -150,7 +150,7 @@ local function info_changed(driver, device, event, args) local preferences = preference_map if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notificaiton') + add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') end end for id, value in pairs(device.preferences) do @@ -229,10 +229,10 @@ local device_init = function(self, device) end device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) else - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.hue.NAME, capabilities.colorControl.hue(1)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl.NAME, capabilities.colorControl.saturation.NAME, capabilities.colorControl.saturation(1)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature(6500)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel.NAME, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl, capabilities.colorControl.hue.NAME, capabilities.colorControl.hue(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl, capabilities.colorControl.saturation.NAME, capabilities.colorControl.saturation(1)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature(6500)) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch("off")) end end 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 91683622d1..e35a1c25e5 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 @@ -37,6 +37,23 @@ local PRIVATE_MODE = "PRIVATE_MODE" local mock_device = test.mock_device.build_test_zigbee_device( { + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 1", + profile = t_utils.get_profile_definition("aqara-switch-no-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.switch.l2aeu1", + server_clusters = { 0x0006 } + } + } + } +) + +local mock_base_device = test.mock_device.build_test_zigbee_device( + { + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 1", profile = t_utils.get_profile_definition("aqara-switch-no-power.yml"), fingerprinted_endpoint_id = 0x01, zigbee_endpoints = { @@ -62,6 +79,7 @@ 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_base_device) test.mock_device.add_test_device(mock_child) end @@ -114,6 +132,30 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Lifecycle - added test", + 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 } }))) + 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, + 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 }))) + end +) + test.register_coroutine_test( "Refresh device", function() 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 b7b4f67b7a..3b8c7187b4 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 @@ -14,11 +14,13 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurementCluster = clusters.ElectricalMeasurement local OnOffCluster = clusters.OnOff local LevelCluster = clusters.Level local SimpleMeteringCluster = clusters.SimpleMetering local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("switch-dimmer-power-energy.yml"), @@ -27,12 +29,14 @@ local mock_device = test.mock_device.build_test_zigbee_device({ id = 1, manufacturer = "Jasco Products", model = "43082", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x000A, 0x0019 } } } }) +zigbee_test_utils.prepare_zigbee_env_info() + local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) @@ -40,6 +44,104 @@ end test.set_test_init_function(test_init) +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + 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, + ElectricalMeasurementCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + LevelCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMeteringCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + LevelCluster.attributes.CurrentLevel:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + LevelCluster.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.register_message_test( "Capability command On should be handled", { 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 new file mode 100755 index 0000000000..41ae8a049b --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua @@ -0,0 +1,484 @@ +-- Copyright 2025 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 test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local BasicCluster = clusters.Basic +local OnOffCluster = clusters.OnOff +local LevelCluster = clusters.Level +local SimpleMeteringCluster = clusters.SimpleMetering +local ElectricalMeasurementCluster = clusters.ElectricalMeasurement +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local utils = require "st.utils" + +local PRIVATE_CLUSTER_ID = 0xFC31 +local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_SCENE_ID =0x00 +local MFG_CODE = 0x122F + +local parent_profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml") +local child_profile = t_utils.get_profile_definition("rgbw-bulb-2700K-6500K.yml") + +local mock_device = test.mock_device.build_test_zigbee_device({ + label = "Inovelli 2-in-1 Blue Series", + profile = parent_profile, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = { 0x0000, 0x0006, 0x0008, 0x0702, 0x0B04 }, + }, + [2] = { + id = 2, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = { 0x0006 }, + }, + [3] = { + id = 3, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = { 0x0006 }, + }, + [4] = { + id = 4, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = { 0x0006 }, + }, + }, + fingerprinted_endpoint_id = 0x01 +}) + +local mock_first_child = test.mock_device.build_test_child_device({ + profile = child_profile, + 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() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(0))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerMeter.power(0))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy(0))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + + test.socket.zigbee:__expect_send({mock_device.id, BasicCluster.attributes.SWBuildID:read(mock_device)}) + + test.mock_device.add_test_device(mock_first_child) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(1))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(1))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(100))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) + +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + 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, + LevelCluster.attributes.CurrentLevel:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Multiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.Divisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x03) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x04) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(3) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(4) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + LevelCluster.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + BasicCluster.attributes.SWBuildID:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + ElectricalMeasurementCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + SimpleMeteringCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + LevelCluster.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 1):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 2):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 3):to_endpoint(3) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 4):to_endpoint(4) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + PRIVATE_CLUSTER_ID, 2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 21, MFG_CODE) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "parameter258 in infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["parameter258"] = "0" } + })) + test.mock_time.advance_time(3) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 258, + MFG_CODE, data_types.Boolean, false) }) + test.socket.zigbee:__expect_send({ mock_device.id, + BasicCluster.attributes.SWBuildID:read(mock_device) }) + end +) + +test.register_coroutine_test( + "parameter22 in infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["parameter22"] = "0" } + })) + test.mock_time.advance_time(3) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 22, + MFG_CODE, data_types.Uint8, 0) }) + test.socket.zigbee:__expect_send({ mock_device.id, + BasicCluster.attributes.SWBuildID:read(mock_device) }) + end +) + + +test.register_message_test( + "Capability on command switch on should be handled : parent device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOffCluster.server.commands.On(mock_device) } + } + } +) + +test.register_coroutine_test( + "Capability on command switch on should be handled : child device", + function() + test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "on", args = {}}}) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(60 * 1) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16803071,4,false,false)) }) + end +) + +test.register_message_test( + "Capability off command switch on should be handled : parent device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOffCluster.server.commands.Off(mock_device) } + } + } +) + +test.register_coroutine_test( + "Capability on command switch on should be handled : child device", + function() + test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "off", args = {}}}) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(60 * 1) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(0,4,false,false)) }) + end +) + + +test.register_message_test( + "Capability setLevel command switch on should be handled : parent device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, LevelCluster.server.commands.MoveToLevelWithOnOff(mock_device, + math.floor(57 * 0xFE / 100), + 0) } + } + } +) + +test.register_coroutine_test( + "Capability setLevel command switch on should be handled : child device", + function() + test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 }}}) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(57))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(60 * 1) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16792063,4,false,false)) }) + end +) + + +test.register_coroutine_test( + "Capability setColorTemperature command switch on should be handled : child device", + function() + test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = { 1800 }}}) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(100))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(60 * 1) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(33514751,4,false,false)) }) + end +) + +test.register_coroutine_test( + "Capability setColor command switch on should be handled : child device", + function() + test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } }}}) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(50))) + test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(50))) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.wait_for_events() + test.mock_time.advance_time(60 * 1) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(25191679,4,false,false)) }) + end +) + +local ENDPOINT = 0x01 +local FRAME_CTRL = 0x1D +local PROFILE_ID = 0x0104 + +local build_scene_message = function(device, payload) + local message = zigbee_test_utils.build_custom_command_id( + device, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_SCENE_ID, + MFG_CODE, + payload, + ENDPOINT + ) + + message.body.zcl_header.frame_ctrl.value = FRAME_CTRL + message.address_header.profile.value = PROFILE_ID + + return message +end + +test.register_coroutine_test( + "Reported private cluster should be handled", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_scene_message(mock_device, "\x01\x01") + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle Power meter", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMeteringCluster.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 60) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 6.0, unit = "W" })) + ) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + ElectricalMeasurementCluster.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 +) + +test.register_coroutine_test( + "Handle Energy meter", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMeteringCluster.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 600) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 6.0, unit = "kWh" })) + ) + end +) + +test.run_registered_tests() 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 6b07420b87..750ef895f2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -26,7 +26,7 @@ local mock_device = test.mock_device.build_test_zigbee_device({ id = 1, manufacturer = "Jasco Products", model = "43078", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0702, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0702, 0x0B04 }, client_clusters = { 0x000A, 0x0019 } } } 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 c1be129215..d533d69489 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 @@ -30,7 +30,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "ROBB smarrt", model = "ROB_200-011-0", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x0019 } } } 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 0c66abd359..d7e4db0f09 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 @@ -30,7 +30,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "ROBB smarrt", model = "ROB_200-014-0", - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04, 0x0B05 }, + server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B04 }, client_clusters = { 0x0019 } } } 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 74598ef3a5..d4d427bc58 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -35,11 +35,43 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) + +local mock_aurora_relay_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("switch-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Aurora", + model = "Smart16ARelay51AU", + server_clusters = {0x0006, 0x0B04, 0x0702} + } + } + } +) + +local mock_vimar_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("switch-power.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Vimar", + model = "Mains_Power_Outlet_v1.0", + server_clusters = {0x0006, 0x0B04, 0x0702} + } + } + } +) + + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_aurora_relay_device) + test.mock_device.add_test_device(mock_vimar_device) end test.set_test_init_function(test_init) @@ -159,4 +191,131 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device :aurora relay", + function() + test.socket.device_lifecycle:__queue_receive({ mock_aurora_relay_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_aurora_relay_device.id, + zigbee_test_utils.build_bind_request(mock_aurora_relay_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_aurora_relay_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_aurora_relay_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_aurora_relay_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_aurora_relay_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_aurora_relay_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_aurora_relay_device, 5, 3600, 5) + } + ) + + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, OnOff.attributes.OnOff:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_aurora_relay_device) }) + test.socket.zigbee:__expect_send({ mock_aurora_relay_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_aurora_relay_device) }) + mock_aurora_relay_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device : vimar", + function() + test.socket.device_lifecycle:__queue_receive({ mock_vimar_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_vimar_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_vimar_device, 1, 15, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_vimar_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_vimar_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_vimar_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_vimar_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_vimar_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send({ + mock_vimar_device.id, + zigbee_test_utils.build_bind_request(mock_vimar_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, OnOff.attributes.OnOff:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_vimar_device) }) + test.socket.zigbee:__expect_send({ mock_vimar_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_vimar_device) }) + mock_vimar_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua new file mode 100755 index 0000000000..45f3d4e2b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua @@ -0,0 +1,93 @@ +-- Copyright 2025 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 test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local BasicCluster = clusters.Basic +local OnOffCluster = clusters.OnOff +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local profile = t_utils.get_profile_definition("basic-switch.yml") + +local mock_device = test.mock_device.build_test_zigbee_device({ + label = "Zigbee Switch", + profile = profile, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "_TZ123fas", + server_clusters = { 0x0006 }, + }, + [2] = { + id = 2, + manufacturer = "_TZ123fas", + server_clusters = { 0x0006 }, + }, + }, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "lifecycle configure event should configure device", + function () + 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, + OnOffCluster.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x02) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 1):to_endpoint(1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + OnOffCluster.ID, 2):to_endpoint(2) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_attribute_read(mock_device, BasicCluster.ID, {0x0004, 0x0000, 0x0001, 0x0005, 0x0007, 0xfffe}) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua deleted file mode 100644 index 9b62d57546..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_dimmer_power_energy.lua +++ /dev/null @@ -1,190 +0,0 @@ --- Copyright 2022 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 test = require "integration_test" -local clusters = require "st.zigbee.zcl.clusters" -local OnOffCluster = clusters.OnOff -local LevelCluster = clusters.Level -local SimpleMeteringCluster = clusters.SimpleMetering -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" - -local mock_device = test.mock_device.build_test_zigbee_device({ - profile = t_utils.get_profile_definition("switch-dimmer-power-energy.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - server_clusters = { 0x0000, 0x0003, 0x0004, 0x0005, 0x0006, 0x0008, 0x0702, 0x0B05 }, - client_clusters = { 0x000A, 0x0019 } - } - } -}) - -local function test_init() - mock_device:set_field("_configuration_version", 1, {persist = true}) - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Capability command On should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.On(mock_device) } - } - } -) - -test.register_message_test( - "Capability command Off should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.Off(mock_device) } - } - } -) - -test.register_message_test( - "Capability command setLevel should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 } } } - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } - } - }, - { - channel = "zigbee", - direction = "send", - message = { - mock_device.id, - LevelCluster.server.commands.MoveToLevelWithOnOff(mock_device, math.floor(57 * 254 / 100)) - } - } - } -) - -test.register_message_test( - "Handle Switch Level", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_device.id, - LevelCluster.attributes.CurrentLevel:build_test_attr_report(mock_device, math.floor(57 / 100 * 254)) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(57)) - }, - { - channel = "devices", - direction = "send", - message = { - "register_native_capability_attr_handler", - { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } - } - }, - } -) - -test.register_message_test( - "Handle Power meter, Sensor value is in kW, capability attribute value is in W", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Divisor:build_test_attr_report(mock_device, 0x0A) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 0x14D) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 33300.0, unit = "W" })) - } - } -) - -test.register_message_test( - "Handle Energy meter", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Multiplier:build_test_attr_report(mock_device, 1) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.Divisor:build_test_attr_report(mock_device, 0x2710) } - }, - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 0x15B3) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.5555, unit = "kWh" })) - } - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua deleted file mode 100644 index d4fcdb3990..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/enbrighten-metering-dimmer/init.lua +++ /dev/null @@ -1,67 +0,0 @@ --- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" -local capabilities = require "st.capabilities" -local constants = require "st.zigbee.constants" -local SimpleMetering = clusters.SimpleMetering -local configurations = require "configurations" - -local ENBRIGHTEN_METERING_DIMMER_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43082" } -} - -local is_enbrighten_metering_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ENBRIGHTEN_METERING_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -local device_init = function(self, device) - local customEnergyDivisor = 10000 - device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) -end - -local do_configure = function(self, device) - device:refresh() - device:configure() -end - -local instantaneous_demand_handler = function(driver, device, value, zb_rx) - local raw_value = value.value - local divisor = 10 - raw_value = raw_value / divisor - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - -local enbrighten_metering_dimmer = { - NAME = "enbrighten metering dimmer", - zigbee_handlers = { - attr = { - [SimpleMetering.ID] = { - [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler - } - } - }, - lifecycle_handlers = { - init = configurations.power_reconfig_wrapper(device_init), - doConfigure = do_configure - }, - can_handle = is_enbrighten_metering_dimmer -} - -return enbrighten_metering_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua index f799a165aa..75f2af82c1 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua @@ -15,7 +15,8 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering -local ElectricalMeasurement = clusters.ElectricalMeasurement +local constants = require "st.zigbee.constants" +local configurations = require "configurations" local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { { mfr = "Jasco Products", model = "43082" } @@ -31,6 +32,11 @@ local is_zigbee_dimmer_power_energy = function(opts, driver, device) return false end +local device_init = function(self, device) + local customEnergyDivisor = 10000 + device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) +end + local do_configure = function(self, device) device:refresh() device:configure() @@ -41,24 +47,29 @@ local do_configure = function(self, device) device:send(SimpleMetering.attributes.Divisor:read(device)) device:send(SimpleMetering.attributes.Multiplier:read(device)) end +end - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end +local instantaneous_demand_handler = function(driver, device, value, zb_rx) + local raw_value = value.value + local divisor = 10 + raw_value = raw_value / divisor + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, capabilities.powerMeter.power({value = raw_value, unit = "W" })) end local zigbee_dimmer_power_energy_handler = { NAME = "zigbee dimmer power energy handler", + zigbee_handlers = { + attr = { + [SimpleMetering.ID] = { + [SimpleMetering.attributes.InstantaneousDemand.ID] = instantaneous_demand_handler + } + } + }, lifecycle_handlers = { + init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - sub_drivers = { - require("zigbee-dimmer-power-energy/enbrighten-metering-dimmer") - }, can_handle = is_zigbee_dimmer_power_energy - } return zigbee_dimmer_power_energy_handler diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index 59424dd784..d3ce34c2fd 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -18,7 +18,7 @@ local clusters = require "st.zigbee.zcl.clusters" local function zll_profile(opts, driver, device, zb_rx, ...) local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] - if (endpoint.profile_id == constants.ZLL_PROFILE_ID) then + if (endpoint ~= nil and endpoint.profile_id == constants.ZLL_PROFILE_ID) then local subdriver = require("zll-polling") return true, subdriver else From 9bbd9f5bc9a4f30c339dd92912e245dbe14e700e Mon Sep 17 00:00:00 2001 From: TJ Andres Date: Wed, 1 Oct 2025 18:57:38 -0500 Subject: [PATCH 174/449] zigbee-switch: Filter incorrect network type A small number of zwave devices failed to migrate correctly and ended up as zwave devices attached to the zigbee switch driver. This change adds a subdriver (non_zigbee_devices) which handles non-zigbee devices safely without crashing the driver. https://smartthings.atlassian.net/browse/CHAD-16558 --- .../SmartThings/zigbee-switch/src/init.lua | 1 + .../src/non_zigbee_devices/init.lua | 57 ++++++++++++++++++ .../src/test/test_bad_device_kind.lua | 58 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 4d5295b3d0..15241cd004 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -140,6 +140,7 @@ local zigbee_switch_driver_template = { capabilities.motionSensor }, sub_drivers = { + lazy_load_if_possible("non_zigbee_devices"), lazy_load_if_possible("hanssem"), lazy_load_if_possible("aqara"), lazy_load_if_possible("aqara-light"), diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua new file mode 100644 index 0000000000..bc6caa8580 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua @@ -0,0 +1,57 @@ +-- Copyright 2025 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. + +-- This is a patch for the zigbee-switch driver to fix https://smartthings.atlassian.net/browse/CHAD-16558 +-- Several hubs were found that had zigbee switch drivers hosting zwave devices. +-- This patch works around it until hubcore 0.59 is released with +-- https://smartthings.atlassian.net/browse/CHAD-16552 + +local st_device = require "st.device" +local log = require "log" + +local function can_handle(opts, driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_ZIGBEE and device.network_type ~= st_device.NETWORK_TYPE_CHILD then + return true, require("non_zigbee_devices") + end + return false +end + +local function device_added(driver, device, event) + log.info(string.format("Non zigbee device added: %s", device)) +end + +local function device_init(driver, device, event) + log.info(string.format("Non zigbee device init: %s", device)) +end + +local function do_configure(driver, device) + log.info(string.format("Non zigbee do configure: %s", device)) +end + +local function info_changed(driver, device, event, args) + log.info(string.format("Non zigbee infoChanged: %s", device)) +end + +local non_zigbee_devices = { + NAME = "non zigbee devices filter", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed + }, + can_handle = can_handle +} + +return non_zigbee_devices \ No newline at end of file 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 new file mode 100644 index 0000000000..3f66d675c9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua @@ -0,0 +1,58 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local dkjson = require 'dkjson' + +-- This test attempts to add a zwave device to this zigbee switch driver +-- Once the monkey-patch is removed with hubcore 59 is released with: +-- https://smartthings.atlassian.net/browse/CHAD-16552 +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("on-off-level.yml"), +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +-- Just validating that the driver doesn't crash is enough to validate +-- that the work-around is effective in ignoring the incorrect device kind +test.register_coroutine_test("zwave_device_handled", function() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", dkjson.encode(mock_device.raw_st_data) }) + test.wait_for_events() + end, + nil +) + +test.register_message_test( + "Capability command for incorrect protocol", + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + } +) + +test.run_registered_tests() From b249b8c3873156200aabe367ca00dc55a17d9644 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 3 Oct 2025 10:34:10 -0500 Subject: [PATCH 175/449] add modular lock subscription updates --- .../matter-lock/src/new-matter-lock/init.lua | 24 +++- .../src/test/test_matter_lock_modular.lua | 126 +++++++++++++++++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 7ae8a26e6f..880f33b4ee 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -285,9 +285,29 @@ local function match_profile_switch(driver, device) device:try_update_metadata({profile = profile_name}) end +local function compare_components(synced_components, prev_components) + if #synced_components ~= #prev_components then + return false + end + for _, component in pairs(synced_components) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return false + end + for _, capability in pairs(component.capabilities) do + if prev_components[component.id][capability.id] == nil then + return false + end + end + end + return true +end + local function info_changed(driver, device, event, args) - if device.profile.id == args.old_st_store.profile.id then - return + if device.profile.id == args.old_st_store.profile.id and + version.api >= 15 and version.rpc >= 9 and -- ignore component check for FW<58 + compare_components(device.profile.components, args.old_st_store.profile.components) then + return end for cap_id, attributes in pairs(subscribed_attributes) do if device:supports_capability_by_id(cap_id) then 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 5dd117a70e..ca903b808b 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 @@ -173,6 +173,45 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter } }) +local mock_device_modular = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-modular-embedded-unlatch.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 = 0x1591, -- PIN & USR & COTA & WDSCH & YDSCH & UNLATCH + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + + local function test_init() test.disable_startup_messages() -- subscribe request @@ -271,6 +310,27 @@ 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_modular() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_modular) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_modular)) + -- add test device + test.mock_device.add_test_device(mock_device_modular) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "added" }) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.matter:__expect_send({mock_device_modular.id, clusters.PowerSource.attributes.AttributeList:read()}) + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "init" }) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_modular.id, "doConfigure" }) + mock_device_modular:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + test.set_test_init_function(test_init) test.register_coroutine_test( @@ -517,7 +577,7 @@ test.register_coroutine_test( }) } ) - test.socket.capability:__expect_send( + test.socket.capability:__expect_send( mock_device_user_pin_schedule_unlatch:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) ) test.socket.capability:__expect_send( @@ -529,4 +589,68 @@ test.register_coroutine_test( { test_init = test_init_user_pin_schedule_unlatch } ) +test.register_coroutine_test( + "Test modular lock profile update (modular to modular) with user, pin. schedule, and unlatch supported. Ensure infoChanged updates subscription", + function() + test.socket.matter:__queue_receive( + { + mock_device_modular.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device_modular, 1, + { + uint32(0), + uint32(1), + uint32(2), + uint32(12), -- BatPercentRemaining + uint32(14), -- BatChargeLevel + uint32(31), + uint32(65528), + uint32(65529), + uint32(65531), + uint32(65532), + uint32(65533), + }) + } + ) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + ) + mock_device_modular:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}} }) + + local updated_device_profile = t_utils.get_profile_definition("lock-modular-embedded-unlatch.yml", + {enabled_optional_capabilities = {{ "main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}, + },} + ) + updated_device_profile.id = "00000000-1111-2222-3333-000000000010" + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({ profile = updated_device_profile })) + + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_modular) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_modular)) + subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device_modular)) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device_modular)) + test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + -- test.socket.capability:__expect_send( + -- mock_device_modular:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + -- ) + + end, + { test_init = test_init_modular } +) + test.run_registered_tests() From 0752c77ccec20d24b82378d2de86e6e6526c0f6b Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 6 Oct 2025 12:17:34 -0500 Subject: [PATCH 176/449] keep SUPPORTED_COMPONENT_CAPABILITIES table on driver --- .../matter-sensor/src/air-quality-sensor/init.lua | 13 ++++--------- drivers/SmartThings/matter-thermostat/src/init.lua | 13 ++++--------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua index 22ce708ea4..37a03d1580 100644 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua @@ -439,15 +439,10 @@ local function driver_switched(driver, device) end local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - if version.api >= 15 and version.rpc >= 9 then - -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) - else - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end + if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) end device:subscribe() end diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 3a03d0a0fd..58f0d75344 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -431,15 +431,10 @@ local endpoint_to_component = function (device, endpoint_id) end local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - if version.api >= 15 and version.rpc >= 9 then - -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) - else - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end + if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) end device:subscribe() device:set_component_to_endpoint_fn(component_to_endpoint) From 63af165d72276337a2771b76baade2bc9dfd6b38 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 29 Sep 2025 14:56:02 -0700 Subject: [PATCH 177/449] WWSTCERT-8129 Heiman Smart water leakage sensor --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index b5ac21330d..9a463e7db6 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -69,6 +69,11 @@ matterManufacturer: vendorId: 0x120B productId: 0x1007 deviceProfileName: co + - id: "4619/4104" + deviceLabel: Smart water leakage sensor + vendorId: 0x120B + productId: 0x1008 + deviceProfileName: leak-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor From 6529516248f7806e8479eb191801723d4c7d32c2 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 30 Sep 2025 10:43:51 -0700 Subject: [PATCH 178/449] WWSTCERT-8141 Decora Smart Wi-Fi ELV Dimmer WWSTCERT-8144 Decora Smart Wi-Fi 0-10V Dimmer WWSTCERT-8148 Decora Smart Wi-Fi (2nd Gen) Mini Plug-In Dimmer WWSTCERT-8151 Decora Smart Wi-Fi (2nd Gen) Tamper Resistant Outlet WWSTCERT-8154 Decora Smart Wi-Fi Mini Plug-In Outlet WWSTCERT-8157 Decora Smart Wi-Fi Outdoor Plug-In Switch WWSTCERT-8160 Decora Evolve Smart Switch Module WWSTCERT-8163 Decora Evolve Smart Dimmer Module --- .../matter-switch/fingerprints.yml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index f1c7b0f4cb..0b62c75b11 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -603,6 +603,46 @@ matterManufacturer: vendorId: 0x109B productId: 0x1000 deviceProfileName: switch-level + - id: "4251/4107" + deviceLabel: Decora Smart Wi-Fi ELV Dimmer + vendorId: 0x109B + productId: 0x100B + deviceProfileName: switch-level + - id: "4251/4110" + deviceLabel: Decora Smart Wi-Fi 0-10V Dimmer + vendorId: 0x109B + productId: 0x100E + deviceProfileName: switch-level + - id: "4251/4108" + deviceLabel: Decora Evolve Smart Dimmer Module + vendorId: 0x109B + productId: 0x100C + deviceProfileName: switch-level + - id: "4251/4109" + deviceLabel: Decora Evolve Smart Switch Module + vendorId: 0x109B + productId: 0x100D + deviceProfileName: switch-binary + - id: "4251/4105" + deviceLabel: Decora Smart Wi-Fi Outdoor Plug-In Switch + vendorId: 0x109B + productId: 0x1009 + deviceProfileName: switch-binary + - id: "4251/4099" + deviceLabel: Leviton Decora Smart Wi-Fi Mini Plug-In Outlet + vendorId: 0x109B + productId: 0x1003 + deviceProfileName: switch-binary + - id: "4251/4100" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Tamper Resistant Outlet + vendorId: 0x109B + productId: 0x1004 + deviceProfileName: switch-binary + - id: "4251/4098" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Mini Plug-In Dimmer + vendorId: 0x109B + productId: 0x1002 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From 3f0cbbed11300e4bd7d0e58210d19fc79daae9f5 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 6 Oct 2025 12:17:34 -0500 Subject: [PATCH 179/449] keep SUPPORTED_COMPONENT_CAPABILITIES table on driver --- .../matter-sensor/src/air-quality-sensor/init.lua | 13 ++++--------- drivers/SmartThings/matter-thermostat/src/init.lua | 13 ++++--------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua index 22ce708ea4..37a03d1580 100644 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua @@ -439,15 +439,10 @@ local function driver_switched(driver, device) end local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - if version.api >= 15 and version.rpc >= 9 then - -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) - else - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end + if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) end device:subscribe() end diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 3a03d0a0fd..58f0d75344 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -431,15 +431,10 @@ local endpoint_to_component = function (device, endpoint_id) end local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - if version.api >= 15 and version.rpc >= 9 then - -- the device used this modular profile workaround on 0.57 FW but no longer requires this table with >=0.58 FW - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, nil) - else - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end + if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) end device:subscribe() device:set_component_to_endpoint_fn(component_to_endpoint) From 0dccd0a85cd5c5a068652ecce8ef515d3d768447 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 6 Oct 2025 14:49:02 -0700 Subject: [PATCH 180/449] WWSTCERT-8030 Zimi Matter Connect --- 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 0b62c75b11..508e481144 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2856,6 +2856,12 @@ matterManufacturer: vendorId: 0x139C productId: 0x0387 deviceProfileName: matter-bridge +#Zimi + - id: "5410/3" + deviceLabel: Zimi Matter Connect + vendorId: 0x1522 + productId: 0x0003 + deviceProfileName: matter-bridge #TUO - id: "5150/1" deviceLabel: "TUO Smart Button" From 706b5c07ec2bd8cabe4991cbc7051d9d1ce06340 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 8 Oct 2025 13:25:22 -0500 Subject: [PATCH 181/449] update test file --- .../matter-lock/src/test/test_matter_lock_modular.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 ca903b808b..d6382faa06 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 @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" @@ -645,10 +644,9 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device_modular:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) - -- test.socket.capability:__expect_send( - -- mock_device_modular:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) - -- ) - + test.socket.capability:__expect_send( + mock_device_modular:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) end, { test_init = test_init_modular } ) From 77808c368dc6799b524b53944ad1460117c4e7cb Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Sat, 11 Oct 2025 13:03:10 -0500 Subject: [PATCH 182/449] remove embedded lockAlarm custom capability def --- .../matter-lock/capabilities/lockAlarm.yml | 27 ------------------- .../src/test/test_aqara_matter_lock.lua | 1 - .../src/test/test_bridged_matter_lock.lua | 1 - .../matter-lock/src/test/test_matter_lock.lua | 1 - .../src/test/test_matter_lock_battery.lua | 1 - .../test/test_matter_lock_batteryLevel.lua | 1 - .../src/test/test_matter_lock_codes.lua | 1 - .../src/test/test_matter_lock_cota.lua | 1 - .../src/test/test_matter_lock_unlatch.lua | 1 - .../src/test/test_new_matter_lock.lua | 1 - .../src/test/test_new_matter_lock_battery.lua | 1 - 11 files changed, 37 deletions(-) delete mode 100644 drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml diff --git a/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml b/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml deleted file mode 100644 index c10f387dbf..0000000000 --- a/drivers/SmartThings/matter-lock/capabilities/lockAlarm.yml +++ /dev/null @@ -1,27 +0,0 @@ -id: lockAlarm -version: 1 -status: proposed -name: Lock Alarm -ephemeral: false -attributes: - alarm: - schema: - type: object - properties: - value: - type: string - enum: - - clear - - lockFactoryReset - - damaged - - forcedOpeningAttempt - - unableToLockTheDoor - - notClosedForALongTime - - highTemperature - - attemptsExceeded - - physicalImpact - additionalProperties: false - required: - - value - enumCommands: [] -commands: {} \ No newline at end of file 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 41ab9840fb..5c4add0ff5 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 @@ -15,7 +15,6 @@ local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" 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 41ecbd612e..0d17cafda6 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 @@ -13,7 +13,6 @@ -- limitations under the License. local test = require "integration_test" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" 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 0123d43239..0db34172d5 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" 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 92af7fcabc..8e0be6359d 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 @@ -13,7 +13,6 @@ -- limitations under the License. local test = require "integration_test" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" 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 d76556a42d..c53547025c 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 @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" 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 ad68ffbe3b..2960d905c2 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 @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local json = require "st.json" local clusters = require "st.matter.clusters" 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 f25fe2a19c..57468ef2fc 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 @@ -14,7 +14,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local json = require "st.json" local clusters = require "st.matter.clusters" 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 421134ec2a..38154d7b04 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 @@ -15,7 +15,6 @@ local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock 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 ab9ac68c88..7d5a59ff32 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 @@ -15,7 +15,6 @@ local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local DoorLock = clusters.DoorLock 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 d58bfb1bdd..6926f1d62d 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 @@ -15,7 +15,6 @@ local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" -test.add_package_capability("lockAlarm.yml") local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" From 96500cce5926a761a2774b07fed301fc67e68078 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:11:42 -0500 Subject: [PATCH 183/449] Matter Energy: Fix non- JSON serializable fields (#2418) Update the SUPPORTED_EVSE_MODES_MAP and SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP fields to use strings as table keys, since datastore tables need to have string keys in order to be JSON serializable. --- .../SmartThings/matter-energy/src/init.lua | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/drivers/SmartThings/matter-energy/src/init.lua b/drivers/SmartThings/matter-energy/src/init.lua index da56397ec3..51d361753a 100644 --- a/drivers/SmartThings/matter-energy/src/init.lua +++ b/drivers/SmartThings/matter-energy/src/init.lua @@ -34,8 +34,8 @@ if version.api < 12 then end local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local SUPPORTED_EVSE_MODES_MAP = "__supported_evse_modes_map" -local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP = "__supported_device_energy_management_modes_map" +local SUPPORTED_EVSE_MODES = "__supported_evse_modes" +local SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES = "__supported_device_energy_management_modes" local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" @@ -47,6 +47,11 @@ local TOTAL_CUMULATIVE_ENERGY_IMPORTED = "__total_cumulative_energy_imported" local TOTAL_CUMULATIVE_ENERGY_EXPORTED = "__total_cumulative_energy_exported" local TOTAL_ACTIVE_POWER = "__total_active_power" +local updated_fields = { + { current_field_name = "__supported_evse_modes_map", updated_field_name = nil }, + { current_field_name = "__supported_device_energy_management_modes_map", updated_field_name = nil } +} + local MAX_CHARGING_CURRENT_CONSTRAINT = 80000 -- In v1.3 release of stack, this check for 80 A is performed. local EVSE_DEVICE_TYPE_ID = 0x050C @@ -55,7 +60,6 @@ local BATTERY_STORAGE_DEVICE_TYPE_ID = 0x0018 local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 local DEVICE_ENERGY_MANAGEMENT_DEVICE_TYPE_ID = 0x050D - local function get_endpoints_for_dt(device, device_type) local endpoints = {} for _, ep in ipairs(device.endpoints) do @@ -100,6 +104,25 @@ local function component_to_endpoint(device, component) end end +local function get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +local function set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +local function check_field_name_updates(device) + for _, field in ipairs(updated_fields) do + if device:get_field(field.current_field_name) then + if field.updated_field_name ~= nil then + device:set_field(field.updated_field_name, device:get_field(field.current_field_name), {persist = true}) + end + device:set_field(field.current_field_name, nil) + end + end +end + local function time_zone_offset() return os.difftime(os.time(), os.time(os.date("!*t", os.time()))) end @@ -193,6 +216,7 @@ local BATTERY_CHARGING_STATE_MAP = { -- Lifecycle Handlers -- local function device_init(driver, device) + check_field_name_updates(device) device:subscribe() device:set_endpoint_to_component_fn(endpoint_to_component) device:set_component_to_endpoint_fn(component_to_endpoint) @@ -386,7 +410,6 @@ local function power_mode_handler(driver, device, ib, response) end local function energy_evse_supported_modes_attr_handler(driver, device, ib, response) - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} local supportedEvseModes = {} for _, mode in ipairs(ib.data.elements) do if version.api < 11 then @@ -394,8 +417,7 @@ local function energy_evse_supported_modes_attr_handler(driver, device, ib, resp end table.insert(supportedEvseModes, mode.elements.label.value) end - supportedEvseModesMap[ib.endpoint_id] = supportedEvseModes - device:set_field(SUPPORTED_EVSE_MODES_MAP, supportedEvseModesMap, { persist = true }) + set_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ib.endpoint_id, supportedEvseModes, { persist = true }) local event = capabilities.mode.supportedModes(supportedEvseModes, { visibility = { displayed = false } }) device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supportedEvseModes, { visibility = { displayed = false } }) @@ -403,10 +425,7 @@ local function energy_evse_supported_modes_attr_handler(driver, device, ib, resp end local function energy_evse_mode_attr_handler(driver, device, ib, response) - device.log.info(string.format("energy_evse_modes_attr_handler currentMode: %s", ib.data.value)) - - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) or {} - local supportedEvseModes = supportedEvseModesMap[ib.endpoint_id] or {} + local supportedEvseModes = get_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ib.endpoint_id) or {} local currentMode = ib.data.value for i, mode in ipairs(supportedEvseModes) do if i - 1 == currentMode then @@ -417,7 +436,6 @@ local function energy_evse_mode_attr_handler(driver, device, ib, response) end local function device_energy_mgmt_supported_modes_attr_handler(driver, device, ib, response) - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} local supportedDeviceEnergyMgmtModes = {} for _, mode in ipairs(ib.data.elements) do if version.api < 12 then @@ -425,8 +443,7 @@ local function device_energy_mgmt_supported_modes_attr_handler(driver, device, i end table.insert(supportedDeviceEnergyMgmtModes, mode.elements.label.value) end - supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] = supportedDeviceEnergyMgmtModes - device:set_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP, supportedDeviceEnergyMgmtModesMap, { persist = true }) + set_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ib.endpoint_id, supportedDeviceEnergyMgmtModes, { persist = true }) local event = capabilities.mode.supportedModes(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) device:emit_event_for_endpoint(ib.endpoint_id, event) event = capabilities.mode.supportedArguments(supportedDeviceEnergyMgmtModes, { visibility = { displayed = false } }) @@ -434,10 +451,7 @@ local function device_energy_mgmt_supported_modes_attr_handler(driver, device, i end local function device_energy_mgmt_mode_attr_handler(driver, device, ib, response) - device.log.info(string.format("device_energy_mgmt_mode_attr_handler currentMode: %s", ib.data.value)) - - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) or {} - local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ib.endpoint_id] or {} + local supportedDeviceEnergyMgmtModes = get_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ib.endpoint_id) or {} local currentMode = ib.data.value for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do if i - 1 == currentMode then @@ -603,8 +617,7 @@ local function handle_set_mode_command(driver, device, cmd) local set_mode_handlers = { ["main"] = function( ... ) local ep = component_to_endpoint(device, cmd.component) - local supportedEvseModesMap = device:get_field(SUPPORTED_EVSE_MODES_MAP) - local supportedEvseModes = supportedEvseModesMap[ep] or {} + local supportedEvseModes = get_field_for_endpoint(device, SUPPORTED_EVSE_MODES, ep) or {} for i, mode in ipairs(supportedEvseModes) do if cmd.args.mode == mode then device:send(clusters.EnergyEvseMode.commands.ChangeToMode(device, ep, i - 1)) @@ -615,8 +628,7 @@ local function handle_set_mode_command(driver, device, cmd) end, ["deviceEnergyManagement"] = function( ... ) local ep = component_to_endpoint(device, cmd.component) - local supportedDeviceEnergyMgmtModesMap = device:get_field(SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES_MAP) - local supportedDeviceEnergyMgmtModes = supportedDeviceEnergyMgmtModesMap[ep] or {} + local supportedDeviceEnergyMgmtModes = get_field_for_endpoint(device, SUPPORTED_DEVICE_ENERGY_MANAGEMENT_MODES, ep) or {} for i, mode in ipairs(supportedDeviceEnergyMgmtModes) do if cmd.args.mode == mode then device:send(clusters.DeviceEnergyManagementMode.commands.ChangeToMode(device, ep, i - 1)) From 8aff21030f8d3b3bce5a28d727c5d7bd650f85f2 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Tue, 29 Jul 2025 09:37:03 -0500 Subject: [PATCH 184/449] chore: Improve Sonos Memory Usage @NoahCornell did some analysis of driver heap usage, and Sonos was disproportionately above the mean for the heap-space-per-device line fit on the dataset. This PR improves memory usage by limiting the information we keep resident from API responses to the fields that we actually use in regular operation. Additional background: One source of heap usage was unbounded memory growth due to task spawning on a certain error pathway, which he already fixed in a different PR. Another source was the fact that we were cacheing the entire API response for the Player and Group information for the LAN's Sonos topology. These API payloads are pretty large, and have actually gotten larger over time. This has been the case for this driver for a very long time; the decision to store the whole response object was made so that we would have the information available if we needed it in the future for bug fixes or enhancements. It turns out that the information we're utilizing hasn't really changed much over the last few years, so I'm feeling quite comfortable about excising the majority of the payload information at this point. We see pretty signifcant memory savings with these changes, and the savings should scale appreciably with device count, which is a big win. --- drivers/SmartThings/sonos/src/api/rest.lua | 2 +- .../sonos/src/api/sonos_connection.lua | 12 +- .../sonos/src/api/sonos_ssdp_discovery.lua | 189 +++++++++++++++--- .../sonos/src/lifecycle_handlers.lua | 2 +- .../SmartThings/sonos/src/sonos_driver.lua | 74 +++---- drivers/SmartThings/sonos/src/sonos_state.lua | 99 +++++---- drivers/SmartThings/sonos/src/types.lua | 40 ++-- 7 files changed, 280 insertions(+), 138 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/rest.lua b/drivers/SmartThings/sonos/src/api/rest.lua index bc2dfe498f..93fbcf0f36 100644 --- a/drivers/SmartThings/sonos/src/api/rest.lua +++ b/drivers/SmartThings/sonos/src/api/rest.lua @@ -74,7 +74,7 @@ local SonosRestApi = {} --- Query a Sonos Group IP address for individual player info ---@param url table a URL table created by `net_url` ---@param headers table? ----@return SonosDiscoveryInfo|SonosErrorResponse|nil +---@return SonosDiscoveryInfoObject|SonosErrorResponse|nil ---@return string|nil error function SonosRestApi.get_player_info(url, headers) url.path = "/api/v1/players/local/info" diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 35b1cf8885..48c8828fc8 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -169,7 +169,7 @@ local function _open_coordinator_socket(sonos_conn, household_id, self_player_id _, err = Router.open_socket_for_player( household_id, coordinator_id, - coordinator.player.websocketUrl, + coordinator.player.websocket_url, api_key ) if err ~= nil then @@ -406,7 +406,7 @@ function SonosConnection.new(driver, device) return end local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -430,7 +430,7 @@ function SonosConnection.new(driver, device) return end local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -453,7 +453,7 @@ function SonosConnection.new(driver, device) return end local group = household.groups[header.groupId] or { playerIds = {} } - for _, player_id in ipairs(group.playerIds) do + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device --- is being deleted so we check for the presence of emit event as a proxy for @@ -484,7 +484,7 @@ function SonosConnection.new(driver, device) return end - local url_ip = lb_utils.force_url_table(coordinator_player.player.websocketUrl).host + local url_ip = lb_utils.force_url_table(coordinator_player.player.websocket_url).host local base_url = lb_utils.force_url_table( string.format("https://%s:%s", url_ip, SonosApi.DEFAULT_SONOS_PORT) ) @@ -510,7 +510,7 @@ function SonosConnection.new(driver, device) end self.driver.sonos:update_household_favorites(header.householdId, new_favorites) - for _, player_id in ipairs(group.playerIds) do + for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index be3ae6092a..5baf30efe8 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -9,7 +9,140 @@ local utils = require "utils" local SonosApi = require "api" local SSDP_SCAN_INTERVAL_SECONDS = 600 +local SONOS_DEFAULT_PORT = 1443 +local SONOS_DEFAULT_WSS_PATH = "websocket/api" +local SONOS_DEFAULT_REST_PATH = "api/v1" + +--- Cached information gathered during discovery scanning, created from a subset of the +--- found [SonosSSDPInfo](lua://SonosSSDPInfo) and [SonosDiscoveryInfoObject](lua://SonosDiscoveryInfoObject) +--- +--- @class SpeakerDiscoveryInfo +--- @field public unique_key UniqueKey +--- @field public mac_addr string +--- @field public expires_at integer +--- @field public ipv4 string +--- @field public port integer +--- @field public household_id HouseholdId +--- @field public player_id PlayerId +--- @field public name string +--- @field public model string +--- @field public model_display_name string +--- @field public sw_gen integer +--- @field public wss_url table +--- @field public rest_url table +--- @field public is_group_coordinator boolean +--- @field public group_name string? nil if a speaker is the non-primary in a bonded set +--- @field public group_id GroupId? nil if a speaker is the non-primary in a bonded set +--- @field package wss_path string? nil if equivalent to the default value; does not include leading slash! +--- @field package rest_path string? nil if equivalent to the default value; does not include leading slash! +local SpeakerDiscoveryInfo = {} + +local proxy_index = function(self, k) + if k == "rest_url" and not rawget(self, "rest_url") then + rawset( + self, + "rest_url", + net_url.parse( + string.format( + "https://%s:%s/%s", + self.ipv4, + self.port, + self.rest_path or SONOS_DEFAULT_REST_PATH + ) + ) + ) + end + + if k == "wss_url" and not rawget(self, "wss_url") then + rawset( + self, + "wss_url", + net_url.parse( + string.format( + "https://%s:%s/%s", + self.ipv4, + self.port, + self.wss_path or SONOS_DEFAULT_WSS_PATH + ) + ) + ) + end + + return rawget(self, k) +end + +local proxy_newindex = function(_, _, _) + error("attempt to index a read-only table", 2) +end + +---@param ssdp_info SonosSSDPInfo +---@param discovery_info SonosDiscoveryInfoObject +---@return SpeakerDiscoveryInfo info +function SpeakerDiscoveryInfo.new(ssdp_info, discovery_info) + local mac_addr = utils.extract_mac_addr(discovery_info.device) + local port, rest_path = string.match(discovery_info.restUrl, "^.*:(%d*)/(.*)$") + local _, wss_path = string.match(discovery_info.websocketUrl, "^.*:(%d*)/(.*)$") + port = tonumber(port) or SONOS_DEFAULT_PORT + + local ret = { + unique_key = utils.sonos_unique_key_from_ssdp(ssdp_info), + expires_at = ssdp_info.expires_at, + ipv4 = ssdp_info.ip, + port = port, + mac_addr = mac_addr, + household_id = ssdp_info.household_id, + player_id = ssdp_info.player_id, + name = discovery_info.device.name, + model = discovery_info.device.model, + model_display_name = discovery_info.device.modelDisplayName, + sw_gen = discovery_info.device.swGen, + is_group_coordinator = ssdp_info.is_group_coordinator, + } + + if type(ssdp_info.group_name) == "string" and #ssdp_info.group_name > 0 then + ret.group_name = ssdp_info.group_name + end + + if type(ssdp_info.group_id) == "string" and #ssdp_info.group_id > 0 then + ret.group_id = ssdp_info.group_id + end + + if type(wss_path) == "string" and #wss_path > 0 and wss_path ~= SONOS_DEFAULT_WSS_PATH then + ret.wss_path = wss_path + end + + if type(rest_path) == "string" and #rest_path > 0 and rest_path ~= SONOS_DEFAULT_REST_PATH then + ret.rest_path = rest_path + end + + for k, v in pairs(SpeakerDiscoveryInfo) do + rawset(ret, k, v) + end + + return setmetatable(ret, { __index = proxy_index, __newindex = proxy_newindex }) +end + +function SpeakerDiscoveryInfo:is_bonded() + return (self.group_id == nil) +end + +---@return SonosSSDPInfo +function SpeakerDiscoveryInfo:as_ssdp_info() + ---@type SonosSSDPInfo + return { + ip = self.ipv4, + group_id = self.group_id or "", + group_name = self.group_name or "", + expires_at = self.expires_at, + player_id = self.player_id, + wss_url = self.wss_url:build(), + household_id = self.household_id, + is_group_coordinator = self.is_group_coordinator, + } +end + local sonos_ssdp = {} +sonos_ssdp.SpeakerDiscoveryInfo = SpeakerDiscoveryInfo ---@module 'luncheon.headers' @@ -147,6 +280,18 @@ function sonos_ssdp.ssdp_info_eq(a, b) and (a.wss_url == b.wss_url) end +---@param disco_info SpeakerDiscoveryInfo +---@param ssdp_info SonosSSDPInfo +function sonos_ssdp.known_speaker_matches_ssdp_info(disco_info, ssdp_info) + return (disco_info.group_id == ssdp_info.group_id) + and (disco_info.group_name == ssdp_info.group_name) + and (disco_info.household_id == ssdp_info.household_id) + and (disco_info.ipv4 == ssdp_info.ip) + and (disco_info.is_group_coordinator == ssdp_info.is_group_coordinator) + and (disco_info.player_id == ssdp_info.player_id) + and (disco_info.wss_url:build() == ssdp_info.wss_url) +end + ---@return SsdpSearchTerm the Sonos ssdp search term ---@return SsdpSearchKwargs the default set of keyword arguments for Sonos function sonos_ssdp.new_search_term_context() @@ -160,8 +305,8 @@ end ---@class SonosPersistentSsdpTask ---@field package ssdp_search_handle SsdpSearchHandle ----@field package player_info_by_sonos_ids table ----@field package player_info_by_mac_addrs table +---@field package player_info_by_sonos_ids table +---@field package player_info_by_mac_addrs table ---@field package waiting_for_unique_key table ---@field package waiting_for_mac_addr table ---@field package control_tx table @@ -217,7 +362,7 @@ function SonosPersistentSsdpTask:get_player_info(reply_tx, ...) end local maybe_existing = lookup_table[lookup_key] - if maybe_existing and maybe_existing.ssdp_info.expires_at > os.time() then + if maybe_existing and maybe_existing.expires_at > os.time() then reply_tx:send(maybe_existing) return end @@ -267,11 +412,12 @@ function sonos_ssdp.spawn_persistent_ssdp_task() local maybe_known = task_handle.player_info_by_sonos_ids[unique_key] local is_new_information = not ( maybe_known - and maybe_known.ssdp_info.expires_at > os.time() - and sonos_ssdp.ssdp_info_eq(maybe_known.ssdp_info, sonos_ssdp_info) + and maybe_known.expires_at > os.time() + and sonos_ssdp.known_speaker_matches_ssdp_info(maybe_known, sonos_ssdp_info) ) - local info_to_send + local speaker_info + local event_bus_msg if is_new_information then local headers = SonosApi.make_headers() @@ -283,30 +429,21 @@ function sonos_ssdp.spawn_persistent_ssdp_task() ) if not discovery_info then log.error(string.format("Error getting discovery info from SSDP response: %s", err)) + elseif discovery_info._objectType == "globalError" then + log.error(string.format("Error message in discovery info: %s", discovery_info.errorCode)) else - local unified_info = - { ssdp_info = sonos_ssdp_info, discovery_info = discovery_info, force_refresh = true } - task_handle.player_info_by_sonos_ids[unique_key] = unified_info - info_to_send = unified_info + speaker_info = SpeakerDiscoveryInfo.new(sonos_ssdp_info, discovery_info) + task_handle.player_info_by_sonos_ids[unique_key] = speaker_info + event_bus_msg = { speaker_info = speaker_info, force_refresh = true } end else - info_to_send = { - ssdp_info = maybe_known.ssdp_info, - discovery_info = maybe_known.discovery_info, - force_refresh = false, - } + speaker_info = maybe_known + event_bus_msg = { speaker_info = speaker_info, force_refresh = false } end - if info_to_send then - if not (info_to_send.discovery_info and info_to_send.discovery_info.device) then - log.error_with( - { hub_logs = false }, - st_utils.stringify_table(info_to_send, "Sonos Discovery Info has unexpected structure") - ) - return - end - event_bus:send(info_to_send) - local mac_addr = utils.extract_mac_addr(info_to_send.discovery_info.device) + if speaker_info then + event_bus:send(event_bus_msg) + local mac_addr = speaker_info.mac_addr local waiting_handles = task_handle.waiting_for_unique_key[unique_key] or {} log.debug(st_utils.stringify_table(waiting_handles, "waiting for unique keys", true)) @@ -318,7 +455,7 @@ function sonos_ssdp.spawn_persistent_ssdp_task() st_utils.stringify_table(waiting_handles, "waiting for unique keys and mac addresses", true) ) for _, reply_tx in ipairs(waiting_handles) do - reply_tx:send(info_to_send) + reply_tx:send(speaker_info) end task_handle.waiting_for_unique_key[unique_key] = {} diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 0db0efc2c9..97a79ac209 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -68,7 +68,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) if not info then device.log.warn(string.format("error receiving device info: %s", recv_err)) else - ---@cast info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } + ---@cast info SpeakerDiscoveryInfo local auth_success, api_key_or_err = driver:check_auth(info) if not auth_success then device:offline() diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 82d88752f2..3d8b22ecf3 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -232,7 +232,7 @@ end --- Check if the driver is able to authenticate against the given household_id --- with what credentials it currently possesses. ----@param info_or_device SonosDevice | { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } +---@param info_or_device SonosDevice | SpeakerDiscoveryInfo ---@return boolean? auth_success true if the driver can authenticate against the provided arguments, false otherwise ---@return string? api_key_or_err if `auth_success` is true, this will be the API key that is known to auth. If `auth_success` is false, this will be nil. If `auth_success` is `nil`, this will be an error message. function SonosDriver:check_auth(info_or_device) @@ -247,13 +247,6 @@ function SonosDriver:check_auth(info_or_device) local rest_url, household_id, sw_gen if type(info_or_device) == "table" then if - type(info_or_device.ssdp_info) == "table" and type(info_or_device.discovery_info) == "table" - then - ---@cast info_or_device { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo } - rest_url = net_url.parse(info_or_device.discovery_info.restUrl) - household_id = info_or_device.ssdp_info.household_id - sw_gen = info_or_device.discovery_info.device.swGen - elseif type(info_or_device.get_field) == "function" and type(info_or_device.set_field) == "function" and info_or_device.id @@ -262,6 +255,11 @@ function SonosDriver:check_auth(info_or_device) rest_url = net_url.parse(info_or_device:get_field(PlayerFields.REST_URL)) household_id = self.sonos:get_sonos_ids_for_device(info_or_device) sw_gen = info_or_device:get_field(PlayerFields.SW_GEN) + else + ---@cast info_or_device SpeakerDiscoveryInfo + rest_url = info_or_device.rest_url + household_id = info_or_device.household_id + sw_gen = info_or_device.sw_gen end end @@ -272,11 +270,7 @@ function SonosDriver:check_auth(info_or_device) ( ( type(info_or_device) == "table" - and ( - info_or_device.label - or info_or_device.id - or info_or_device.discovery_info.device.name - ) + and (info_or_device.label or info_or_device.id or info_or_device.name) ) or "" ) ) @@ -443,33 +437,32 @@ local function make_ssdp_event_handler( end end if receiver == discovery_event_subscription then - ---@type { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean }? + ---@type { speaker_info: SpeakerDiscoveryInfo, force_refresh: boolean } local event, recv_err = discovery_event_subscription:receive() if event then - local mac_addr = utils.extract_mac_addr(event.discovery_info.device) - local unique_key = utils.sonos_unique_key_from_ssdp(event.ssdp_info) + local speaker_info = event.speaker_info if event.force_refresh or not ( - unauthorized[unique_key] - or discovered[unique_key] - or driver.bonded_devices[mac_addr] + unauthorized[speaker_info.unique_key] + or discovered[speaker_info.unique_key] + or driver.bonded_devices[speaker_info.mac_addr] ) then - local _, api_key = driver:check_auth(event) + local _, api_key = driver:check_auth(event.speaker_info) local success, handle_err, err_code = - driver:handle_player_discovery_info(api_key, event) + driver:handle_player_discovery_info(api_key, event.speaker_info) if not success then if err_code == "ERROR_NOT_AUTHORIZED" then - unauthorized[unique_key] = event + unauthorized[speaker_info.unique_key] = event end log.warn_with( { hub_logs = false }, string.format("Failed to handle discovered speaker: %s", handle_err) ) else - discovered[unique_key] = true + discovered[speaker_info.unique_key] = true end end else @@ -503,20 +496,14 @@ function SonosDriver:start_ssdp_event_task() end ---@param api_key string ----@param info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo, force_refresh: boolean } +---@param info SpeakerDiscoveryInfo ---@param device SonosDevice? ---@return boolean|nil response nil or false on failure ---@return nil|string error the error reason on failure, nil on success ---@return nil|string error_code the Sonos error code, if available function SonosDriver:handle_player_discovery_info(api_key, info, device) - -- If the SSDP Group Info is an empty string, then that means it's the non-primary - -- speaker in a bonded set (e.g. a home theater system, a stereo pair, etc). - -- These aren't the same as speaker groups, and bonded speakers can't be controlled - -- via websocket at all. So we ignore all bonded non-primary speakers if they are not - -- already onboarded. - - local discovery_info_mac_addr = utils.extract_mac_addr(info.discovery_info.device) - local bonded = (#info.ssdp_info.group_id == 0) + local discovery_info_mac_addr = info.mac_addr + local bonded = info:is_bonded() self.bonded_devices[discovery_info_mac_addr] = bonded local maybe_device = self:get_device_by_dni(discovery_info_mac_addr) @@ -527,11 +514,11 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) api_key = api_key or self:get_fallback_api_key() - local rest_url = net_url.parse(info.discovery_info.restUrl) + local rest_url = info.rest_url local maybe_token, no_token_reason = self:get_oauth_token() local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = - SonosApi.RestApi.get_groups_info(rest_url, info.ssdp_info.household_id, headers) + SonosApi.RestApi.get_groups_info(rest_url, info.household_id, headers) if response_err then return nil, string.format("Error while making REST API call: %s", response_err) @@ -541,7 +528,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) local additional_info = response.reason or response.wwwAuthenticate local error_string = string.format( '`getGroups` response error for player "%s":\n\tError Code: %s', - info.discovery_info.device.name, + info.name, response.errorCode ) @@ -558,7 +545,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) return nil, error_string, response.errorCode end - local sw_gen = info.discovery_info.device.swGen + local sw_gen = info.sw_gen local is_s1 = sw_gen == 1 local response_valid if is_s1 then @@ -577,12 +564,11 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end --- @cast response SonosGroupsResponseBody - self.sonos:update_household_info(info.ssdp_info.household_id, response, self) + self.sonos:update_household_info(info.household_id, response, self) local device_to_update, device_mac_addr - local maybe_device_id = - self.sonos:get_device_id_for_player(info.ssdp_info.household_id, info.discovery_info.playerId) + local maybe_device_id = self.sonos:get_device_id_for_player(info.household_id, info.player_id) if device then device_to_update = device @@ -596,7 +582,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) end if not device_mac_addr then - if not (info and info.discovery_info and info.discovery_info.device) then + if not (info and info.mac_addr) then return nil, st_utils.stringify_table(info, "Sonos Discovery Info has unexpected structure") end device_mac_addr = discovery_info_mac_addr @@ -613,10 +599,8 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) self.dni_to_device_id[device_mac_addr] = device_to_update.id self.sonos:associate_device_record(device_to_update, info) elseif not bonded then - local name = info.discovery_info.device.name - or info.discovery_info.device.modelDisplayName - or "Unknown Sonos Player" - local model = info.discovery_info.device.modelDisplayName or "Unknown Sonos Model" + local name = info.name or info.model_display_name or "Unknown Sonos Player" + local model = info.model_display_name or "Unknown Sonos Model" local try_create_message = { type = "LAN", device_network_id = device_mac_addr, @@ -624,7 +608,7 @@ function SonosDriver:handle_player_discovery_info(api_key, info, device) label = name, model = model, profile = "sonos-player", - vendor_provided_label = info.discovery_info.device.model, + vendor_provided_label = info.model, } self:try_create_device(try_create_message) diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index 12d10f6211..4fe27ccdc6 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -10,8 +10,8 @@ local SonosConnection = require "api.sonos_connection" --- @class SonosHousehold --- Information on an entire Sonos system ("household"), such as its current groups, list of players, etc. --- @field public id HouseholdId ---- @field public groups table All of the current groups in the system ---- @field public players table All of the current players in the system +--- @field public groups table All of the current groups in the system +--- @field public players table All of the current players in the system --- @field public bonded_players table PlayerID's in this map that map to true are non-primary bonded players, and not controllable. --- @field public player_to_group table quick lookup from Player ID -> Group ID --- @field public st_devices table Player ID -> ST Device Record UUID information for the household @@ -77,7 +77,7 @@ end local _STATE = { ---@type Households households = make_households_table(), - ---@type table + ---@type table device_record_map = {}, } @@ -87,12 +87,12 @@ local SonosState = {} SonosState.__index = SonosState ---@param device SonosDevice ----@param info { ssdp_info: SonosSSDPInfo, discovery_info: SonosDiscoveryInfo } +---@param info SpeakerDiscoveryInfo function SonosState:associate_device_record(device, info) - local household_id = info.ssdp_info.household_id - local group_id = info.ssdp_info.group_id - -- This is the device id even if the device is a secondary in a bonded set - local player_id = info.discovery_info.playerId + local household_id = info.household_id + local group_id = info.group_id + -- This is the device id even if the device is a seondary in a bonded set + local player_id = info.player_id local household = _STATE.households[household_id] if not household then @@ -120,7 +120,9 @@ function SonosState:associate_device_record(device, info) return end - local player = (household.players[player_id] or {}).player + local player_tbl = household.players[player_id] + local player = (player_tbl or {}).player + local sonos_device = (player_tbl or {}).device if not player then log.error( @@ -134,30 +136,31 @@ function SonosState:associate_device_record(device, info) household.st_devices[player_id] = device.id - _STATE.device_record_map[device.id] = - { sonos_device_id = player_id, group = group, player = player, household = household } + _STATE.device_record_map[device.id] = { + sonos_device_id = player_id, + group = group, + player = player, + household = household, + sonos_device = sonos_device, + } local bonded = household.bonded_players[player_id] and true or false - local sw_gen_changed = utils.update_field_if_changed( - device, - PlayerFields.SW_GEN, - info.discovery_info.device.swGen, - { persist = true } - ) + local sw_gen_changed = + utils.update_field_if_changed(device, PlayerFields.SW_GEN, info.sw_gen, { persist = true }) if sw_gen_changed then - CapEventHandlers.handle_sw_gen(device, info.discovery_info.device.swGen) + CapEventHandlers.handle_sw_gen(device, info.sw_gen) end - device:set_field(PlayerFields.REST_URL, info.discovery_info.restUrl, { persist = true }) + device:set_field(PlayerFields.REST_URL, info.rest_url:build(), { persist = true }) local sonos_conn = device:get_field(PlayerFields.CONNECTION) local connected = sonos_conn ~= nil local websocket_url_changed = utils.update_field_if_changed( device, PlayerFields.WSS_URL, - info.ssdp_info.wss_url, + info.wss_url:build(), { persist = true } ) @@ -176,12 +179,8 @@ function SonosState:associate_device_record(device, info) { persist = true } ) - local player_id_changed = utils.update_field_if_changed( - device, - PlayerFields.PLAYER_ID, - player_id, - { persist = true } - ) + local player_id_changed = + utils.update_field_if_changed(device, PlayerFields.PLAYER_ID, player_id, { persist = true }) local need_refresh = connected and (websocket_url_changed or household_id_changed or player_id_changed) @@ -206,7 +205,7 @@ function SonosState:associate_device_record(device, info) end ---@param household SonosHousehold ----@param group SonosGroupObject +---@param group SonosGroupInfo ---@param device SonosDevice function SonosState:update_device_record_group_info(household, group, device) local player_id = device:get_field(PlayerFields.PLAYER_ID) @@ -221,10 +220,10 @@ function SonosState:update_device_record_group_info(household, group, device) and player_id and group and group.id - and group.coordinatorId - ) and player_id == group.coordinatorId + and group.coordinator_id + ) and player_id == group.coordinator_id then - local player_ids_list = (household.groups[group.id] or {}).playerIds or {} + local player_ids_list = (household.groups[group.id] or {}).player_ids or {} if #player_ids_list > 1 then group_role = "primary" else @@ -249,11 +248,11 @@ function SonosState:update_device_record_group_info(household, group, device) field_changed = utils.update_field_if_changed( device, PlayerFields.COORDINATOR_ID, - group.coordinatorId, + group.coordinator_id, { persist = true } ) if not bonded and field_changed then - CapEventHandlers.handle_group_coordinator_update(device, group.coordinatorId) + CapEventHandlers.handle_group_coordinator_update(device, group.coordinator_id) end if bonded then @@ -306,9 +305,21 @@ function SonosState:update_device_record_from_state(household_id, device) self:update_device_record_group_info(household, current_mapping.group, device) end --- Helper function for when updating household info +--- Helper function for when updating household info +---@param driver SonosDriver +---@param player SonosPlayerObject +---@param household SonosHousehold +---@param known_bonded_players table +---@param sonos_device_id PlayerId local function update_device_info(driver, player, household, known_bonded_players, sonos_device_id) - household.players[sonos_device_id] = { player = player } + ---@type SonosDeviceInfo + local device_info = { id = sonos_device_id, primary_device_id = player.id } + ---@type SonosPlayerInfo + local player_info = { id = player.id, websocket_url = player.websocketUrl } + household.players[sonos_device_id] = { + player = player_info, + device = device_info, + } local previously_bonded = known_bonded_players[sonos_device_id] and true or false local currently_bonded local group_id @@ -328,8 +339,8 @@ local function update_device_info(driver, player, household, known_bonded_player _STATE.device_record_map[maybe_device_id] = _STATE.device_record_map[maybe_device_id] or {} _STATE.device_record_map[maybe_device_id].household = household _STATE.device_record_map[maybe_device_id].group = household.groups[group_id] - _STATE.device_record_map[maybe_device_id].player = player - _STATE.device_record_map[maybe_device_id].sonos_device_id = sonos_device_id + _STATE.device_record_map[maybe_device_id].player = player_info + _STATE.device_record_map[maybe_device_id].sonos_device = device_info if previously_bonded ~= currently_bonded then local target_device = driver:get_device_info(maybe_device_id) if target_device then @@ -351,7 +362,8 @@ function SonosState:update_household_info(id, groups_event, driver) local groups, players = groups_event.groups, groups_event.players for _, group in ipairs(groups) do - household.groups[group.id] = group + household.groups[group.id] = + { id = group.id, coordinator_id = group.coordinatorId, player_ids = group.playerIds } for _, playerId in ipairs(group.playerIds) do household.player_to_group[playerId] = group.id end @@ -363,11 +375,11 @@ function SonosState:update_household_info(id, groups_event, driver) for _, player in ipairs(players) do -- Prefer devices because deviceIds is deprecated but all we care about is -- the ID so either way is fine. - if player.devices then + if type(player.devices) == "table" then for _, device in ipairs(player.devices) do update_device_info(driver, player, household, known_bonded_players, device.id) end - elseif player.deviceIds then + elseif type(player.deviceIds) == "table" then for _, device_id in ipairs(player.deviceIds) do update_device_info(driver, player, household, known_bonded_players, device_id) end @@ -378,7 +390,10 @@ function SonosState:update_household_info(id, groups_event, driver) end end if log_devices_error then - log.warn_with( { hub_logs = true}, "Group event contained neither devices nor deviceIds in player") + log.warn_with( + { hub_logs = true }, + "Group event contained neither devices nor deviceIds in player" + ) end household.id = id @@ -474,7 +489,7 @@ function SonosState:get_coordinator_for_group(household_id, group_id) return end - return group.coordinatorId + return group.coordinator_id end --- @param device SonosDevice @@ -583,7 +598,7 @@ function SonosState:get_coordinator_for_device(device) ) end - return household_id, group.coordinatorId, nil + return household_id, group.coordinator_id, nil end ---@return SonosState diff --git a/drivers/SmartThings/sonos/src/types.lua b/drivers/SmartThings/sonos/src/types.lua index a1be63fe3f..c404f92a36 100644 --- a/drivers/SmartThings/sonos/src/types.lua +++ b/drivers/SmartThings/sonos/src/types.lua @@ -5,6 +5,9 @@ --- @alias HouseholdId string --- @alias GroupId string +--------- #region Sonos API Types; the following defintions are from the Sonos API +--------- In particular, anything ending in `Object` is an API object. + --- @alias SonosCapabilities ---| "PLAYBACK" # The player can produce audio. You can target it for playback. ---| "CLOUD" # The player can send commands and receive events over the internet. @@ -17,11 +20,11 @@ ---| "SPEAKER_DETECTION" # The component device is capable of detecting connected speaker drivers. ---| "FIXED_VOLUME" # The device supports fixed volume. ---- @class SonosFeatureInfo +--- @class SonosFeatureInfoObject --- @field public _objectType "feature" --- @field public name string ----@class SonosVersionsInfo +---@class SonosVersionsInfoObject ---@field public _objectType "sdkVersions" ---@field public audioTxProtocol { [1]: integer } ---@field public trueplaySdk { [1]: string } @@ -43,11 +46,11 @@ --- @field public softwareVersion string Stores the software version the player is running. --- @field public hwVersion string Stores the hardware version the player is running. The format is: `{vendor}.{model}.{submodel}.{revision}-{region}.` --- @field public swGen integer Stores the software generation that the player is running. ---- @field public versions SonosVersionsInfo ---- @field public features SonosFeatureInfo[] +--- @field public versions SonosVersionsInfoObject +--- @field public features SonosFeatureInfoObject[] --- Lua representation of the Sonos `discoveryInfo` JSON object: https://developer.sonos.com/build/control-sonos-players-lan/discover-lan/#discoveryInfo-object ---- @class SonosDiscoveryInfo +--- @class SonosDiscoveryInfoObject --- @field public _objectType "discoveryInfo" --- @field public device SonosDeviceInfoObject The device object. This object presents immutable data that describes a Sonos device. Use this object to uniquely identify any Sonos device. See below for details. --- @field public householdId HouseholdId An opaque identifier assigned to the device during registration. This field may be missing prior to registration. @@ -143,11 +146,7 @@ --- @field public capabilities SonosCapabilities[] --- @field public devices SonosDeviceInfoObject[] ---- Sonos player local state ---- @class PlayerDiscoveryState ---- @field public info_cache SonosDiscoveryInfo Table representation of the JSON returned by the player REST API info endpoint ---- @field public ipv4 string the ipv4 address of the player on the local network ---- @field public is_coordinator boolean whether or not the player was a coordinator (at time of discovery) +--------- #endregion Sonos API Types --- @class SonosSSDPInfo --- Information parsed from Sonos SSDP reply. Contains most of what is needed to uniquely @@ -165,13 +164,7 @@ --- @field public expires_at integer --- @alias SonosFavorites { id: string, name: string }[] ---- @alias DiscoCallback fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfo, group_info: SonosGroupsResponseBody): boolean? - ----@class SonosFieldCacheTable ----@field public swGen number ----@field public household_id string ----@field public player_id string ----@field public wss_url string +--- @alias DiscoCallback fun(dni: string, ssdp_group_info: SonosSSDPInfo, player_info: SonosDiscoveryInfoObject, group_info: SonosGroupsResponseBody): boolean? --- Sonos Player device --- @class SonosDevice : st.Device @@ -189,5 +182,18 @@ --- @field public emit_event fun(self: SonosDevice, event: any) --- @field public driver SonosDriver +--- @class SonosGroupInfo +--- @field public id GroupId +--- @field public coordinator_id PlayerId +--- @field public player_ids PlayerId[] + +--- @class SonosDeviceInfo +--- @field public id PlayerId +--- @field public primary_device_id PlayerId? + +--- @class SonosPlayerInfo +--- @field public id PlayerId +--- @field public websocket_url string + --- Sonos JSON commands --- @class SonosCommand From 240eb3234cd2f8b20dbbc005645b66420531c4ce Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 13 Oct 2025 11:36:44 -0700 Subject: [PATCH 185/449] CHAD-15785 Z-Wave: WindowShadePreset capability updates (#2292) * CHAD-15785 Z-Wave: WindowShadePreset capability updates * update driver to work with earlier versions of lua_libs --- .../window-treatment-preset-reverse.yml | 2 - .../src/iblinds-window-treatment/init.lua | 8 ++- .../src/iblinds-window-treatment/v3.lua | 12 ++++ .../zwave-window-treatment/src/init.lua | 25 +++++++ .../src/springs-window-fashion-shade/init.lua | 13 ++++ .../test_zwave_iblinds_window_treatment.lua | 25 +++++-- .../test_zwave_springs_window_treatment.lua | 4 ++ .../src/test/test_zwave_window_treatment.lua | 12 ++++ .../src/window_preset_defaults.lua | 65 +++++++++++++++++++ 9 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua diff --git a/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml b/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml index a786609ae2..b1b46c51ef 100644 --- a/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml +++ b/drivers/SmartThings/zwave-window-treatment/profiles/window-treatment-preset-reverse.yml @@ -15,7 +15,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua index 2b981cd7ab..690e50d694 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua @@ -15,6 +15,7 @@ local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=3 }) +local window_preset_defaults = require "window_preset_defaults" local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS = { {mfr = 0x0287, prod = 0x0003, model = 0x000D}, -- iBlinds Window Treatment v1 / v2 @@ -70,8 +71,11 @@ function capability_handlers.set_shade_level(driver, device, command) set_shade_level_helper(driver, device, command.args.shadeLevel) end -function capability_handlers.preset_position(driver, device) - set_shade_level_helper(driver, device, device.preferences.presetPosition or 50) +function capability_handlers.preset_position(driver, device, command) + local level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or 50 + set_shade_level_helper(driver, device, level) end local iblinds_window_treatment = { diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua index abc16b678c..9e0a38775d 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua @@ -35,6 +35,15 @@ local function can_handle_iblinds_window_treatment_v3(opts, driver, device, ...) return false end +local function init_handler(self, device) + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.supportedCommands.NAME) == nil then + + -- setPresetPosition is not supported (device uses a separate preference) + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition"}, { visibility = { displayed = false }})) + end +end + local capability_handlers = {} function capability_handlers.close(driver, device) @@ -65,6 +74,9 @@ function capability_handlers.preset_position(driver, device) end local iblinds_window_treatment_v3 = { + lifecycle_handlers = { + init = init_handler + }, capability_handlers = { [capabilities.windowShade.ID] = { [capabilities.windowShade.commands.close.NAME] = capability_handlers.close diff --git a/drivers/SmartThings/zwave-window-treatment/src/init.lua b/drivers/SmartThings/zwave-window-treatment/src/init.lua index 1cc9c34141..aaaf5c7889 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/init.lua @@ -20,6 +20,24 @@ local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) local preferencesMap = require "preferences" +local window_preset_defaults = require "window_preset_defaults" + +local function init_handler(self, device) + 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 relocated to `added` after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + window_preset_defaults.PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_preset_defaults.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end local function added_handler(self, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, { visibility = { displayed = false } })) @@ -56,9 +74,16 @@ local driver_template = { capabilities.battery }, lifecycle_handlers = { + init = init_handler, added = added_handler, infoChanged = info_changed }, + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_preset_defaults.set_preset_position_cmd, + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_preset_defaults.window_shade_preset_cmd, + } + }, sub_drivers = { require("springs-window-fashion-shade"), require("iblinds-window-treatment"), diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua index 6e8ef08b09..e501eecdff 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua @@ -37,6 +37,16 @@ local function can_handle_springs_window_fashion_shade(opts, driver, device, ... return false end +local function init_handler(self, device) + -- This device has a preset position set in hardware, so we need to override the base driver + if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and + device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.supportedCommands.NAME) == nil then + + -- setPresetPosition is not supported + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition"}, { visibility = { displayed = false }})) + end +end + local capability_handlers = {} --- Issue a window shade preset position command to the specified device. @@ -58,6 +68,9 @@ function capability_handlers.preset_position(driver, device) end local springs_window_fashion_shade = { + lifecycle_handlers = { + init = init_handler + }, capability_handlers = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index f1a841c626..f53e540fc4 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -49,6 +49,15 @@ local mock_blind_v3 = test.mock_device.build_test_zwave_device({ local function test_init() test.mock_device.add_test_device(mock_blind) test.mock_device.add_test_device(mock_blind_v3) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition"}, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) @@ -240,13 +249,15 @@ test.register_coroutine_test( test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.zwave:__set_channel_ordering("relaxed") test.socket.device_lifecycle():__queue_receive({mock_blind.id, "init"}) - test.socket.device_lifecycle():__queue_receive(mock_blind:generate_info_changed( - { - preferences = { - presetPosition = 35 - } - } - )) + test.socket.capability:__queue_receive( + { + mock_blind.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {35} } + } + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadePreset.position(35)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua index 0b2d209f2a..be3b3a6405 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua @@ -18,6 +18,7 @@ local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" -- supported comand classes: SWITCH_MULTILEVEL local window_shade_switch_multilevel_endpoints = { @@ -38,6 +39,9 @@ local mock_springs_window_fashion_shade = test.mock_device.build_test_zwave_devi local function test_init() test.mock_device.add_test_device(mock_springs_window_fashion_shade) + test.socket.capability:__expect_send( + mock_springs_window_fashion_shade:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition"}, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index bc3a087093..534f6c56f7 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -54,6 +54,18 @@ local mock_window_shade_switch_multilevel = test.mock_device.build_test_zwave_de local function test_init() test.mock_device.add_test_device(mock_window_shade_basic) test.mock_device.add_test_device(mock_window_shade_switch_multilevel) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_switch_multilevel:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_window_shade_switch_multilevel:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua new file mode 100644 index 0000000000..5046940dea --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua @@ -0,0 +1,65 @@ +-- Copyright 2025 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. + + +-- These were added to scripting engine, but this file is to make sure drivers +-- runing on older versions of scripting engine can still access these values +local capabilities = require "st.capabilities" +local constants = require "st.zwave.constants" + +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) +--- @type st.zwave.CommandClass.Basic +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) + +local defaults = {} + +--- WINDOW SHADE PRESET CONSTANTS +defaults.PRESET_LEVEL = 50 +defaults.PRESET_LEVEL_KEY = "_presetLevel" + +defaults.set_preset_position_cmd = function(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(defaults.PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + +defaults.window_shade_preset_cmd = function(driver, device, command) + local set + local get + local preset_level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(constants.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + defaults.PRESET_LEVEL + if device:is_cc_supported(cc.SWITCH_MULTILEVEL) then + set = SwitchMultilevel:Set({ + value = preset_level, + duration = constants.DEFAULT_DIMMING_DURATION + }) + get = SwitchMultilevel:Get({}) + else + set = Basic:Set({ + value = preset_level + }) + get = Basic:Get({}) + end + device:send_to_component(set, command.component) + local query_device = function() + device:send_to_component(get, command.component) + end + device.thread:call_with_delay(constants.MIN_DIMMING_GET_STATUS_DELAY, query_device) +end + +return defaults \ No newline at end of file From 8a193a8250a5fecfd5e971bcf00af915e00ce186 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 13 Oct 2025 11:36:54 -0700 Subject: [PATCH 186/449] CHAD-15784 Zigbee: WindowShadePreset capability updates (#2282) * CHAD-15784 Zigbee: WindowShadePreset capability updates * update init and added handlers, tests * fixup * remove preference * update drivers to work on older lua_libs versions * fixup --- .gitignore | 1 + .../profiles/window-treatment-battery.yml | 3 -- .../profiles/window-treatment-powerSource.yml | 5 +- ...w-treatment-profile-no-firmware-update.yml | 5 +- .../profiles/window-treatment-profile.yml | 4 -- .../profiles/window-treatment-reverse.yml | 2 - .../src/axis/axis_version/init.lua | 4 +- .../zigbee-window-treatment/src/axis/init.lua | 4 +- .../src/feibit/init.lua | 4 +- .../src/hanssem/init.lua | 4 +- .../zigbee-window-treatment/src/init.lua | 25 ++++++++++ .../src/invert-lift-percentage/init.lua | 6 +-- .../src/screen-innovations/init.lua | 6 +-- .../src/somfy/init.lua | 4 +- .../test_zigbee_window_shade_battery_ikea.lua | 17 ++++++- ...est_zigbee_window_shade_battery_yoolax.lua | 12 ++++- .../src/test/test_zigbee_window_treatment.lua | 46 ++++++++++++++++++- .../test_zigbee_window_treatment_axis.lua | 14 ++++-- .../test_zigbee_window_treatment_feibit.lua | 37 +++++++++++++-- .../test_zigbee_window_treatment_hanssem.lua | 17 ++++++- ...ee_window_treatment_screen_innovations.lua | 9 +++- .../test_zigbee_window_treatment_somfy.lua | 43 +++++++++-------- .../src/vimar/init.lua | 18 +++++++- .../src/window_shade_utils.lua | 44 ++++++++++++++++++ .../src/yoolax/init.lua | 4 +- .../tuya-zigbee/src/curtain/init.lua | 37 +++++++++++++-- .../src/test/test_tuya_curtain.lua | 16 ++++++- 27 files changed, 316 insertions(+), 75 deletions(-) create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua diff --git a/.gitignore b/.gitignore index ea01709e90..c5bbdc6f99 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ lua_libs-api_* tools/test_output/* tools/coverage_output/* .DS_Store +.venv/ 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 275697cb27..be0ae73d76 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml @@ -16,6 +16,3 @@ components: version: 1 categories: - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true 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 7c5255527f..7323ee0218 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml @@ -17,7 +17,4 @@ components: - id: firmwareUpdate version: 1 categories: - - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true \ No newline at end of file + - name: Blind \ No newline at end of file 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 ded16d963b..8e641e6b42 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 @@ -11,7 +11,4 @@ components: - id: refresh version: 1 categories: - - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true + - name: Blind \ No newline at end of file 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 933dd85321..b10a5ea101 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml @@ -14,7 +14,3 @@ components: version: 1 categories: - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true - 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 91b21811f9..0eb666a5d2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml @@ -18,5 +18,3 @@ components: preferences: - preferenceId: reverse explicit: true - - preferenceId: presetPosition - explicit: true diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua index 1bc09cbb6b..c0682d73c0 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua @@ -13,7 +13,7 @@ -- limitations under the License. local capabilities = require "st.capabilities" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -52,7 +52,7 @@ local function window_shade_level_cmd_handler(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) window_shade_set_level(device, command, level) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua index acd67d2d32..a47de06df7 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua @@ -14,7 +14,7 @@ local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -50,7 +50,7 @@ local function window_shade_level_cmd_handler(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) window_shade_set_level(device, command, level) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua index 0c1d20be11..f33bf81cbd 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua @@ -14,7 +14,7 @@ local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local window_shade_defaults = require "st.zigbee.defaults.windowShade_defaults" local device_management = require "st.zigbee.device_management" local Level = zcl_clusters.Level @@ -50,7 +50,7 @@ local function level_attr_handler(driver, device, value, zb_rx) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) set_shade_level(device, level, command.component) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua index bc974706b6..be956d79b2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua @@ -16,7 +16,7 @@ local Messages = require "st.zigbee.messages" local data_types = require "st.zigbee.data_types" local ZigbeeConstants = require "st.zigbee.constants" local generic_body = require "st.zigbee.generic_body" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local TUYA_CLUSTER = 0xEF00 local DP_TYPE_VALUE = "\x02" @@ -148,7 +148,7 @@ local function SetShadeLevelHandler(driver, device, capability_command) end local function PresetPositionHandler(driver, device, capability_command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, capability_command.component) SetShadeLevelHandler(driver, device, {args = { shadeLevel = level }}) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index fc3535c043..933e0d6977 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -15,6 +15,24 @@ local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" +local window_shade_utils = require "window_shade_utils" + +local function init_handler(self, device) + 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 relocated to `added` after migration is complete + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + + local preset_position = device:get_field(window_shade_utils.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + window_shade_utils.PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_shade_utils.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end local function added_handler(self, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, { visibility = { displayed = false }})) @@ -40,8 +58,15 @@ local zigbee_window_treatment_driver_template = { require("hanssem"), require("screen-innovations")}, lifecycle_handlers = { + init = init_handler, added = added_handler }, + capability_handlers = { + [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, + } + }, health_check = false, } diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua index 62c3afc7eb..19532bc847 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua @@ -14,6 +14,7 @@ local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" +local window_shade_utils = require "window_shade_utils" local WindowCovering = zcl_clusters.WindowCovering @@ -75,9 +76,8 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - if device.preferences ~= nil and device.preferences.presetPosition ~= nil then - set_shade_level(device, device.preferences.presetPosition, command) - end + local level = window_shade_utils.get_preset_level(device, command.component) + set_shade_level(device, level, command) end local ikea_window_treatment = { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua index 11d4ff17f9..868e92af56 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua @@ -15,7 +15,7 @@ -- require st provided libraries local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local device_management = require "st.zigbee.device_management" local utils = require "st.utils" @@ -52,9 +52,9 @@ end -- this is window_shade_preset_cmd local function window_shade_preset_cmd(driver, device, command) - local go_to_level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) -- send levels without inverting as: 0% closed (i.e., open) to 100% closed - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, go_to_level)) + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) end -- this is device_added diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua index c90a0c833a..ffc9541b64 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua @@ -14,7 +14,7 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local WindowCovering = zcl_clusters.WindowCovering @@ -109,7 +109,7 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) command.args.shadeLevel = level window_shade_level_cmd(driver, device, command) end 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 4fee02c00f..4fe49568eb 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 @@ -37,7 +37,14 @@ 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) + 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) @@ -162,7 +169,13 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 30}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {30}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(30))) test.wait_for_events() test.socket.capability:__queue_receive( { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua index 106730e7fd..d51f303378 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua @@ -67,7 +67,14 @@ end 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) + 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) @@ -136,9 +143,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly added devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 70) + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) }) end ) 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 b177eaacec..6380e5ce51 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 @@ -25,7 +25,14 @@ 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) + 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) @@ -219,7 +226,42 @@ test.register_message_test( mock_device.id, clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) } - } + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "setPresetPosition", args = {20} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadePreset.position(20)) + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "presetPosition", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 20) + } + }, } ) 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 3148f48965..5068f889a5 100755 --- 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 @@ -43,7 +43,14 @@ 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) + 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) @@ -458,8 +465,9 @@ test.register_coroutine_test( } ) test.socket.capability:__set_channel_ordering("relaxed") + -- freshly joined devices will ignore the preference value test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(30)) + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) ) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) @@ -467,7 +475,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100 - 30) + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100 - 50) } ) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua index 747895e515..c65d38281a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua @@ -37,7 +37,14 @@ 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) + 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) @@ -221,9 +228,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly joined devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(30/100 * 254)) + Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(50/100 * 254)) }) end ) @@ -257,9 +265,10 @@ test.register_coroutine_test( { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } } ) + -- newly joined devices will ignore the preference test.socket.zigbee:__expect_send({ mock_device.id, - Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(100/100 * 254)) + Level.server.commands.MoveToLevelWithOnOff(mock_device,math.floor(50/100 * 254)) }) end ) @@ -267,7 +276,16 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value = 1 ", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 10}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {1} } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(1)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -285,7 +303,16 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with a positive preset value of < 1", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 0}})) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {0} } + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(0)) + ) test.wait_for_events() test.socket.capability:__queue_receive( { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index a0e0fbf844..23814eff28 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -43,7 +43,14 @@ 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) + 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) @@ -278,7 +285,13 @@ test.register_coroutine_test( test.register_coroutine_test( "Preset position handler", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 30}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {30}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(30))) test.wait_for_events() test.socket.zigbee:__queue_receive({ 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 717731124e..b7f630cf71 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 @@ -45,7 +45,14 @@ 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) + 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) 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 2e7c140f17..c861b3470c 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 @@ -38,7 +38,14 @@ 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) + 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) @@ -356,7 +363,13 @@ test.register_message_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value of 1", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 1}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {1}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(1))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -374,7 +387,13 @@ test.register_coroutine_test( test.register_coroutine_test( "windowShadePreset capability should be handled with preset value of 100", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 100}})) + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {100}} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(100))) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -408,27 +427,15 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "windowShadePreset capability should be handled with preset value of > 100", + "windowShadePreset capability should be handled with preset value of < 1 (but positive)", function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 101}})) - test.wait_for_events() test.socket.capability:__queue_receive( { mock_device.id, - { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + { capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = {0}} } ) - test.socket.zigbee:__expect_send({ - mock_device.id, - WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) - }) - end -) - -test.register_coroutine_test( - "windowShadePreset capability should be handled with preset value of < 1 (but positive)", - function() - test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {presetPosition = 0}})) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadePreset.position(0))) test.wait_for_events() test.socket.capability:__queue_receive( { diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua index 6d2d6bb11d..9fa928645a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua @@ -14,7 +14,7 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" +local window_shade_utils = require "window_shade_utils" local zcl_clusters = require "st.zigbee.zcl.clusters" local WindowCovering = zcl_clusters.WindowCovering local windowShade = capabilities.windowShade.windowShade @@ -124,7 +124,7 @@ end -- COMMAND HANDLER for PresetPosition local function window_shade_preset_handler(driver, device, command) - local level = device.preferences.presetPosition or device:get_field(window_preset_defaults.PRESET_LEVEL_KEY) or window_preset_defaults.PRESET_LEVEL + local level = window_shade_utils.get_preset_level(device, command.component) command.args.shadeLevel = level window_shade_set_level_handler(driver, device, command) end @@ -134,6 +134,20 @@ local device_init = function(self, device) -- Reset Status device:set_field(VIMAR_SHADES_CLOSING, false) device:set_field(VIMAR_SHADES_OPENING, false) + + -- for windowshadepreset update migration + 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 = window_shade_utils.get_preset_level(device, "main") + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(window_shade_utils.PRESET_LEVEL_KEY, preset_position, {persist = true}) + end end -- DRIVER HANDLER CONFIGURATION diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua new file mode 100644 index 0000000000..262e549c2d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -0,0 +1,44 @@ +-- Copyright 2025 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 zcl_clusters = require "st.zigbee.zcl.clusters" + +local utils = {} + +utils.PRESET_LEVEL = 50 +utils.PRESET_LEVEL_KEY = "_presetLevel" + +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 + (device.preferences ~= nil and device.preferences.presetPosition) or + utils.PRESET_LEVEL + + return level +end + +utils.window_shade_preset_cmd = function(driver, device, command) + local level = device:get_latest_state(command.component, "windowShadePreset", "position") or + device:get_field(utils.PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + utils.PRESET_LEVEL + device:send_to_component(command.component, zcl_clusters.WindowCovering.server.commands.GoToLiftPercentage(device, level)) +end + +utils.set_preset_position_cmd = function(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + +return utils \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua index d98924f54a..46cb33fed2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua @@ -17,6 +17,7 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local zcl_global_commands = require "st.zigbee.zcl.global_commands" local Status = require "st.zigbee.generated.types.ZclStatus" local WindowCovering = zcl_clusters.WindowCovering +local window_shade_utils = require "window_shade_utils" local device_management = require "st.zigbee.device_management" @@ -79,7 +80,8 @@ local function window_shade_level_cmd(driver, device, command) end local function window_shade_preset_cmd(driver, device, command) - set_shade_level(driver, device, device.preferences.presetPosition, command) + local level = window_shade_utils.get_preset_level(device, command.component) + set_shade_level(driver, device, level, command) end local function set_window_shade_level(level) diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 1d4d494815..03c34cac65 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -17,10 +17,12 @@ local clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" local device_management = require "st.zigbee.device_management" local tuya_utils = require "tuya_utils" -local window_preset_defaults = require "st.zigbee.defaults.windowShadePreset_defaults" local Basic = clusters.Basic local packet_id = 0 +local PRESET_LEVEL = 50 +local PRESET_LEVEL_KEY = "_presetLevel" + local FINGERPRINTS = { { mfr = "_TZE284_nladmfvf", model = "TS0601"} } @@ -34,6 +36,23 @@ local function is_tuya_curtain(opts, driver, device) return false end +local function init_handler(self, device) + 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 + PRESET_LEVEL + + device:emit_event(capabilities.windowShadePreset.position(preset_position, { visibility = {displayed = false}})) + device:set_field(PRESET_LEVEL_KEY, preset_position, {persist = true}) + end +end + local do_configure = function(driver, device) -- configure ApplicationVersion to keep device online, tuya hub also uses this attribute tuya_utils.send_magic_spell(device) @@ -45,6 +64,8 @@ local function device_added(driver, device) device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, { visibility = { displayed = false }})) + tuya_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadePreset, capabilities.windowShadePreset.position.NAME, PRESET_LEVEL) end local function increase_packet_id(packet_id) @@ -97,11 +118,19 @@ local function window_shade_level(driver, device, command) end local function window_shade_preset(driver, device) - local level = device.preferences and device.preferences.presetPosition or window_preset_defaults.PRESET_LEVEL + local level = device:get_latest_state("main", "windowShadePreset", "position") or + device:get_field(PRESET_LEVEL_KEY) or + (device.preferences ~= nil and device.preferences.presetPosition) or + PRESET_LEVEL tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", level), packet_id) packet_id = increase_packet_id(packet_id) end +local function set_preset_position_cmd(driver, device, command) + device:emit_component_event({id = command.component}, capabilities.windowShadePreset.position(command.args.position)) + device:set_field(PRESET_LEVEL_KEY, command.args.position, {persist = true}) +end + local function tuya_cluster_handler(driver, device, zb_rx) local window_shade_level_event, window_shade_val_event local raw = zb_rx.body.zcl_body.body_bytes @@ -127,6 +156,7 @@ end local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { + init = init_handler, added = device_added, infoChanged = device_info_changed, doConfigure = do_configure @@ -141,7 +171,8 @@ local tuya_curtain_driver = { [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level }, [capabilities.windowShadePreset.ID] = { - [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset, + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = set_preset_position_cmd } }, zigbee_handlers = { 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 44f11a09f4..7a0ea0b215 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -39,7 +39,14 @@ local mock_simple_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_simple_device)end + test.mock_device.add_test_device(mock_simple_device) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end test.set_test_init_function(test_init) @@ -108,6 +115,10 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closed() ) ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.wait_for_events() -- Avoid sending the initial window shade event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) test.socket.capability:__expect_send( @@ -117,6 +128,9 @@ 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 ) From 07532ad86b1cca91b50124e61b2d4465abd98d09 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 13 Oct 2025 13:41:36 -0500 Subject: [PATCH 187/449] Matter Window Covering: Update preset handling (#2295) This change utilizes the setPresetPosition command that was recently added to the windowCoveringPreset capability to set the preset rather than using an embedded preference. --- .../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 | 29 +++++++++-- .../src/test/test_matter_window_covering.lua | 52 ++++++++++++++----- 8 files changed, 63 insertions(+), 30 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 56cac5ffa4..a1896ca634 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true 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 73165b4a90..fa46d87cc6 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true 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 4164fb88d0..903565e68c 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml @@ -18,7 +18,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true 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 49b255830e..27f7f1f61a 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 @@ -19,7 +19,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true 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 4ce9636939..c6a759b610 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml @@ -17,7 +17,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml index b588b41ec9..fbd40ed08d 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml @@ -15,7 +15,5 @@ components: categories: - name: Blind preferences: - - preferenceId: presetPosition - explicit: true - preferenceId: reverse explicit: true diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index b3dd31a05c..289c164e64 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -28,6 +28,8 @@ local battery_support = { BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" } local REVERSE_POLARITY = "__reverse_polarity" +local PRESET_LEVEL_KEY = "__preset_level_key" +local PRESET_LEVEL = 50 local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -69,6 +71,17 @@ end local function device_init(driver, device) 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 + 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 @@ -122,22 +135,29 @@ local function handle_preset(driver, device, cmd) local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) if #lift_eps > 0 then - local lift_value = 100 - device.preferences.presetPosition - local hundredths_lift_percent = lift_value * 100 + local lift_value = device:get_latest_state( + "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME + ) or PRESET_LEVEL + local hundredths_lift_percent = (100 - lift_value) * 100 local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( device, endpoint_id, hundredths_lift_percent ) device:send(req) end if #tilt_eps > 0 then - -- Use default preset tilt percentage to 50 until a canonical preference is created for preset tilt position local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( - device, endpoint_id, 50 * 100 + device, endpoint_id, PRESET_LEVEL * 100 ) device:send(req) end 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) @@ -345,6 +365,7 @@ local matter_driver_template = { capability_handlers = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_preset, + [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = handle_set_preset, }, [capabilities.windowShade.ID] = { [capabilities.windowShade.commands.close.NAME] = handle_close, 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 4c1eab7443..b30f2ea4bf 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 @@ -26,7 +26,6 @@ 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}, - preferences = { presetPosition = 30 }, endpoints = { { endpoint_id = 2, @@ -58,7 +57,6 @@ local mock_device_mains_powered = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - preferences = { presetPosition = 30 }, endpoints = { { endpoint_id = 2, @@ -100,6 +98,19 @@ local CLUSTER_SUBSCRIBE_LIST_NO_BATTERY = { WindowCovering.server.attributes.OperationalStatus, } +local function set_preset(device) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + device:generate_test_message( + "main", capabilities.windowShadePreset.position(50, {visibility = {displayed = false}}) + ) + ) +end + local function test_init() test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -111,6 +122,7 @@ local function test_init() ) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + set_preset(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 @@ -134,6 +146,7 @@ local function test_init_mains_powered() ) test.socket.device_lifecycle:__queue_receive({ mock_device_mains_powered.id, "init" }) + set_preset(mock_device_mains_powered) local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_BATTERY[1]:subscribe(mock_device_mains_powered) for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_BATTERY) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_mains_powered)) end @@ -756,20 +769,31 @@ test.register_coroutine_test("OperationalStatus report contains current position ) end) -test.register_coroutine_test("Handle windowcoveringPreset", function() - test.socket.capability:__queue_receive( - { +test.register_coroutine_test( + "Handle preset commands", + function() + local PRESET_LEVEL = 30 + test.socket.capability:__queue_receive({ + mock_device.id, + {capability = "windowShadePreset", component = "main", command = "setPresetPosition", args = { PRESET_LEVEL }}, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadePreset.position(PRESET_LEVEL) + ) + ) + test.socket.capability:__queue_receive({ mock_device.id, {capability = "windowShadePreset", component = "main", command = "presetPosition", args = {}}, - } - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 7000)} - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 5000)} - ) -end) + }) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, (100 - PRESET_LEVEL) * 100)} + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 5000)} + ) + end +) test.register_coroutine_test( "Test profile change to window-covering-battery when battery percent remaining attribute (attribute ID 12) is available", From 8511a240d06b1fd6e07507f8d9cb69edcf219bab Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 6 Oct 2025 14:49:02 -0700 Subject: [PATCH 188/449] WWSTCERT-8030 Zimi Matter Connect --- 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 0b62c75b11..508e481144 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2856,6 +2856,12 @@ matterManufacturer: vendorId: 0x139C productId: 0x0387 deviceProfileName: matter-bridge +#Zimi + - id: "5410/3" + deviceLabel: Zimi Matter Connect + vendorId: 0x1522 + productId: 0x0003 + deviceProfileName: matter-bridge #TUO - id: "5150/1" deviceLabel: "TUO Smart Button" From dc6e165431f9ee7c9f3c9d2ab0ffe77d8f911215 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 14 Oct 2025 16:19:59 -0700 Subject: [PATCH 189/449] WWSTCERT-8274 Aqara Smart Lock U200 Lite --- 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 bd5cc13d0c..27722d9207 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -10,6 +10,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2801 deviceProfileName: lock-user-pin + - id: "4447/10247" + deviceLabel: Aqara Smart Lock U200 Lite + vendorId: 0x115F + productId: 0x2807 + deviceProfileName: lock-battery #Eufy - id: "5427/1" deviceLabel: eufy Smart Lock E31 From d891f62d585318392455dd2fa659902e06050904 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Tue, 14 Oct 2025 18:24:23 -0500 Subject: [PATCH 190/449] simplify logic --- .../matter-lock/src/new-matter-lock/init.lua | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 880f33b4ee..cbab5a4db0 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -33,6 +33,8 @@ 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 MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED" + local RESPONSE_STATUS_MAP = { [DoorLock.types.DlStatus.SUCCESS] = "success", [DoorLock.types.DlStatus.FAILURE] = "failure", @@ -248,6 +250,7 @@ local function match_profile_modular(driver, device) table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) + device:set_field(MODULAR_PROFILE_UPDATED, true) end local function match_profile_switch(driver, device) @@ -285,30 +288,11 @@ local function match_profile_switch(driver, device) device:try_update_metadata({profile = profile_name}) end -local function compare_components(synced_components, prev_components) - if #synced_components ~= #prev_components then - return false - end - for _, component in pairs(synced_components) do - if (prev_components[component.id] == nil) or - (#component.capabilities ~= #prev_components[component.id].capabilities) then - return false - end - for _, capability in pairs(component.capabilities) do - if prev_components[component.id][capability.id] == nil then - return false - end - end - end - return true -end - local function info_changed(driver, device, event, args) - if device.profile.id == args.old_st_store.profile.id and - version.api >= 15 and version.rpc >= 9 and -- ignore component check for FW<58 - compare_components(device.profile.components, args.old_st_store.profile.components) then - return + if device.profile.id == args.old_st_store.profile.id and not device:get_field(MODULAR_PROFILE_UPDATED) then + return end + device:set_field(MODULAR_PROFILE_UPDATED, nil) for cap_id, attributes in pairs(subscribed_attributes) do if device:supports_capability_by_id(cap_id) then for _, attr in ipairs(attributes) do From 4d11fed2aaf8b4bdc7ae739744ebaa9ab1f018f4 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Thu, 9 Oct 2025 15:04:40 -0500 Subject: [PATCH 191/449] Move top level requires into functions --- .../SmartThings/zigbee-switch/src/init.lua | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 15241cd004..5d5a575cdd 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -12,16 +12,14 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +-- The only reason we need this is because of supported_capabilities on the driver template local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" -local clusters = require "st.zigbee.zcl.clusters" local configurationMap = require "configurations" -local zcl_global_commands = require "st.zigbee.zcl.global_commands" -local SimpleMetering = clusters.SimpleMetering -local ElectricalMeasurement = clusters.ElectricalMeasurement -local preferences = require "preferences" -local device_lib = require "st.device" +local CONFIGURE_REPORTING_RESPONSE_ID = 0x07 +local SIMPLE_METERING_ID = 0x0702 +local ELECTRICAL_MEASUREMENT_ID = 0x0B04 local function lazy_load_if_possible(sub_driver_name) -- gets the current lua libs api version @@ -37,6 +35,7 @@ local function lazy_load_if_possible(sub_driver_name) end local function info_changed(self, device, event, args) + local preferences = require "preferences" preferences.update_preferences(self, device, args) end @@ -46,9 +45,10 @@ local do_configure = function(self, device) -- Additional one time configuration if device:supports_capability(capabilities.energyMeter) or device:supports_capability(capabilities.powerMeter) then + local clusters = require "st.zigbee.zcl.clusters" -- Divisor and multipler for EnergyMeter - device:send(SimpleMetering.attributes.Divisor:read(device)) - device:send(SimpleMetering.attributes.Multiplier:read(device)) + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) end end @@ -85,6 +85,7 @@ local device_init = function(driver, device) if ias_zone_config_method ~= nil then device:set_ias_zone_config_method(ias_zone_config_method) end + local device_lib = require "st.device" if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then device:set_find_child(find_child) end @@ -102,6 +103,9 @@ local function is_mcd_device(device) end local function device_added(driver, device, event) + local clusters = require "st.zigbee.zcl.clusters" + local device_lib = require "st.device" + local main_endpoint = device:get_endpoint(clusters.OnOff.ID) if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then for _, ep in ipairs(device.zigbee_endpoints) do @@ -171,11 +175,11 @@ local zigbee_switch_driver_template = { }, zigbee_handlers = { global = { - [SimpleMetering.ID] = { - [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + [SIMPLE_METERING_ID] = { + [CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response }, - [ElectricalMeasurement.ID] = { - [zcl_global_commands.CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response + [ELECTRICAL_MEASUREMENT_ID] = { + [CONFIGURE_REPORTING_RESPONSE_ID] = configurationMap.handle_reporting_config_response } } }, From 5710939fc8506eeac133e820516864643a378048 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Thu, 9 Oct 2025 15:05:52 -0500 Subject: [PATCH 192/449] Move configurations so it can be optimized --- .../src/{configurations.lua => configurations/init.lua} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename drivers/SmartThings/zigbee-switch/src/{configurations.lua => configurations/init.lua} (100%) diff --git a/drivers/SmartThings/zigbee-switch/src/configurations.lua b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua similarity index 100% rename from drivers/SmartThings/zigbee-switch/src/configurations.lua rename to drivers/SmartThings/zigbee-switch/src/configurations/init.lua From 629f45c9cd49b03352567fa51dfe32b5fb501c33 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Thu, 9 Oct 2025 15:07:00 -0500 Subject: [PATCH 193/449] Optimize configurations module to avoid devices table penalty on startup --- .../src/configurations/devices.lua | 112 +++++++++++++ .../zigbee-switch/src/configurations/init.lua | 158 +++--------------- 2 files changed, 137 insertions(+), 133 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/configurations/devices.lua diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua new file mode 100644 index 0000000000..b26478884a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua @@ -0,0 +1,112 @@ +local clusters = require "st.zigbee.zcl.clusters" +local ColorControl = clusters.ColorControl +local IASZone = clusters.IASZone +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local Alarms = clusters.Alarms +local constants = require "st.zigbee.constants" + +local devices = { + IKEA_RGB_BULB = { + FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI bulb E27 CWS opal 600lm" }, + { mfr = "IKEA of Sweden", model = "TRADFRI bulb E26 CWS opal 600lm" } + }, + CONFIGURATION = { + { + cluster = ColorControl.ID, + attribute = ColorControl.attributes.CurrentX.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = ColorControl.attributes.CurrentX.base_type, + reportable_change = 16 + }, + { + cluster = ColorControl.ID, + attribute = ColorControl.attributes.CurrentY.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = ColorControl.attributes.CurrentY.base_type, + reportable_change = 16 + } + } + }, + SENGLED_BULB_WITH_MOTION_SENSOR = { + FINGERPRINTS = { + { mfr = "sengled", model = "E13-N11" } + }, + CONFIGURATION = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 30, + maximum_interval = 300, + data_type = IASZone.attributes.ZoneStatus.base_type + } + }, + IAS_ZONE_CONFIG_METHOD = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE + }, + FRIENT_SWITCHES = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "SPLZB-131" }, + { mfr = "frient A/S", model = "SPLZB-132" }, + { mfr = "frient A/S", model = "SPLZB-134" }, + { mfr = "frient A/S", model = "SPLZB-137" }, + { mfr = "frient A/S", model = "SPLZB-141" }, + { mfr = "frient A/S", model = "SPLZB-142" }, + { mfr = "frient A/S", model = "SPLZB-144" }, + { mfr = "frient A/S", model = "SPLZB-147" }, + { mfr = "frient A/S", model = "SMRZB-143" }, + { mfr = "frient A/S", model = "SMRZB-153" }, + { mfr = "frient A/S", model = "SMRZB-332" }, + { mfr = "frient A/S", model = "SMRZB-342" } + }, + CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, + reportable_change = 1 + },{ + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, + reportable_change = 1 + },{ + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 1 + },{ + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 1 + },{ + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 1 + },{ + cluster = Alarms.ID, + attribute = Alarms.attributes.AlarmCount.ID, + minimum_interval = 1, + maximum_interval = 3600, + data_type = Alarms.attributes.AlarmCount.base_type, + reportable_change = 1, + }, + } + }, +} + +return devices \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua index f465046e72..2e1c4e2fc9 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua @@ -12,146 +12,32 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -local clusters = require "st.zigbee.zcl.clusters" -local constants = require "st.zigbee.constants" -local capabilities = require "st.capabilities" -local device_def = require "st.device" - -local ColorControl = clusters.ColorControl -local IASZone = clusters.IASZone -local ElectricalMeasurement = clusters.ElectricalMeasurement -local SimpleMetering = clusters.SimpleMetering -local Alarms = clusters.Alarms -local Status = require "st.zigbee.generated.types.ZclStatus" local CONFIGURATION_VERSION_KEY = "_configuration_version" local CONFIGURATION_ATTEMPTED = "_reconfiguration_attempted" -local devices = { - IKEA_RGB_BULB = { - FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI bulb E27 CWS opal 600lm" }, - { mfr = "IKEA of Sweden", model = "TRADFRI bulb E26 CWS opal 600lm" } - }, - CONFIGURATION = { - { - cluster = ColorControl.ID, - attribute = ColorControl.attributes.CurrentX.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = ColorControl.attributes.CurrentX.base_type, - reportable_change = 16 - }, - { - cluster = ColorControl.ID, - attribute = ColorControl.attributes.CurrentY.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = ColorControl.attributes.CurrentY.base_type, - reportable_change = 16 - } - } - }, - SENGLED_BULB_WITH_MOTION_SENSOR = { - FINGERPRINTS = { - { mfr = "sengled", model = "E13-N11" } - }, - CONFIGURATION = { - { - cluster = IASZone.ID, - attribute = IASZone.attributes.ZoneStatus.ID, - minimum_interval = 30, - maximum_interval = 300, - data_type = IASZone.attributes.ZoneStatus.base_type - } - }, - IAS_ZONE_CONFIG_METHOD = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE - }, - FRIENT_SWITCHES = { - FINGERPRINTS = { - { mfr = "frient A/S", model = "SPLZB-131" }, - { mfr = "frient A/S", model = "SPLZB-132" }, - { mfr = "frient A/S", model = "SPLZB-134" }, - { mfr = "frient A/S", model = "SPLZB-137" }, - { mfr = "frient A/S", model = "SPLZB-141" }, - { mfr = "frient A/S", model = "SPLZB-142" }, - { mfr = "frient A/S", model = "SPLZB-144" }, - { mfr = "frient A/S", model = "SPLZB-147" }, - { mfr = "frient A/S", model = "SMRZB-143" }, - { mfr = "frient A/S", model = "SMRZB-153" }, - { mfr = "frient A/S", model = "SMRZB-332" }, - { mfr = "frient A/S", model = "SMRZB-342" } - }, - CONFIGURATION = { - { - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, - reportable_change = 1 - },{ - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, - reportable_change = 1 - },{ - cluster = ElectricalMeasurement.ID, - attribute = ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 1 - },{ - cluster = SimpleMetering.ID, - attribute = SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 1 - },{ - cluster = SimpleMetering.ID, - attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, - reportable_change = 1 - },{ - cluster = Alarms.ID, - attribute = Alarms.attributes.AlarmCount.ID, - minimum_interval = 1, - maximum_interval = 3600, - data_type = Alarms.attributes.AlarmCount.base_type, - reportable_change = 1, - }, - } - }, -} - - local configurations = {} -local active_power_configuration = { - cluster = clusters.ElectricalMeasurement.ID, - attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, - reportable_change = 5 -} - -local instantaneous_demand_configuration = { - cluster = clusters.SimpleMetering.ID, - attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, - minimum_interval = 5, - maximum_interval = 3600, - data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, - reportable_change = 5 -} - configurations.check_and_reconfig_devices = function(driver) + local clusters = require "st.zigbee.zcl.clusters" + local capabilities = require "st.capabilities" + local instantaneous_demand_configuration = { + cluster = clusters.SimpleMetering.ID, + attribute = clusters.SimpleMetering.attributes.InstantaneousDemand.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.SimpleMetering.attributes.InstantaneousDemand.base_type, + reportable_change = 5 + } + local active_power_configuration = { + cluster = clusters.ElectricalMeasurement.ID, + attribute = clusters.ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 5, + maximum_interval = 3600, + data_type = clusters.ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 5 + } + for device_id, device in pairs(driver.device_cache) do local config_version = device:get_field(CONFIGURATION_VERSION_KEY) if config_version == nil or config_version < driver.current_config_version then @@ -174,6 +60,10 @@ configurations.check_and_reconfig_devices = function(driver) end configurations.handle_reporting_config_response = function(driver, device, zb_mess) + local Status = require "st.zigbee.generated.types.ZclStatus" + local clusters = require "st.zigbee.zcl.clusters" + local device_def = require "st.device" + local dev = device local find_child_fn = device:get_field(device_def.FIND_CHILD_KEY) if find_child_fn ~= nil then @@ -219,6 +109,7 @@ configurations.power_reconfig_wrapper = function(orig_function) end configurations.get_device_configuration = function(zigbee_device) + local devices = require "configurations.devices" for _, device in pairs(devices) do for _, fingerprint in pairs(device.FINGERPRINTS) do if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then @@ -230,6 +121,7 @@ configurations.get_device_configuration = function(zigbee_device) end configurations.get_ias_zone_config_method = function(zigbee_device) + local devices = require "configurations.devices" for _, device in pairs(devices) do for _, fingerprint in pairs(device.FINGERPRINTS) do if zigbee_device:get_manufacturer() == fingerprint.mfr and zigbee_device:get_model() == fingerprint.model then From bd1b06f55385f72bdb097bbab460f1b66ee55045 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 10 Oct 2025 09:39:24 -0500 Subject: [PATCH 194/449] Use lazy handler for some lifecycle handlers at the top level driver --- .../src/configurations/devices.lua | 14 ++++ .../SmartThings/zigbee-switch/src/init.lua | 80 +++---------------- .../src/lifecycle_handlers/device_added.lua | 56 +++++++++++++ .../src/lifecycle_handlers/do_configure.lua | 27 +++++++ .../src/lifecycle_handlers/find_child.lua | 17 ++++ .../src/lifecycle_handlers/info_changed.lua | 18 +++++ 6 files changed, 144 insertions(+), 68 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua index b26478884a..51fd1c3665 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua @@ -1,3 +1,17 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" local ColorControl = clusters.ColorControl local IASZone = clusters.IASZone diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 5d5a575cdd..81c39297e3 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -20,10 +20,17 @@ local configurationMap = require "configurations" local CONFIGURE_REPORTING_RESPONSE_ID = 0x07 local SIMPLE_METERING_ID = 0x0702 local ELECTRICAL_MEASUREMENT_ID = 0x0B04 +local version = require "version" + +local lazy_handler +if version.api >= 15 then + lazy_handler = require "st.utils.lazy_handler" +else + lazy_handler = require +end local function lazy_load_if_possible(sub_driver_name) -- gets the current lua libs api version - local version = require "version" -- version 9 will include the lazy loading functions if version.api >= 9 then @@ -34,24 +41,6 @@ local function lazy_load_if_possible(sub_driver_name) end -local function info_changed(self, device, event, args) - local preferences = require "preferences" - preferences.update_preferences(self, device, args) -end - -local do_configure = function(self, device) - device:refresh() - device:configure() - - -- Additional one time configuration - if device:supports_capability(capabilities.energyMeter) or device:supports_capability(capabilities.powerMeter) then - local clusters = require "st.zigbee.zcl.clusters" - -- Divisor and multipler for EnergyMeter - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - end -end - local function component_to_endpoint(device, component_id) local ep_num = component_id:match("switch(%d)") return ep_num and tonumber(ep_num) or device.fingerprinted_endpoint_id @@ -66,10 +55,6 @@ local function endpoint_to_component(device, ep) end end -local function find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) -end - local device_init = function(driver, device) device:set_component_to_endpoint_fn(component_to_endpoint) device:set_endpoint_to_component_fn(endpoint_to_component) @@ -87,52 +72,11 @@ local device_init = function(driver, device) end local device_lib = require "st.device" if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + local find_child = require "lifecycle_handlers.find_child" device:set_find_child(find_child) end end -local function is_mcd_device(device) - local components = device.profile.components - if type(components) == "table" then - local component_count = 0 - for _, component in pairs(components) do - component_count = component_count + 1 - end - return component_count >= 2 - end -end - -local function device_added(driver, device, event) - local clusters = require "st.zigbee.zcl.clusters" - local device_lib = require "st.device" - - local main_endpoint = device:get_endpoint(clusters.OnOff.ID) - if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then - for _, ep in ipairs(device.zigbee_endpoints) do - if ep.id ~= main_endpoint then - if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then - device:set_find_child(find_child) - if find_child(device, ep.id) == nil then - local name = string.format("%s %d", device.label, ep.id) - local child_profile = "basic-switch" - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%02X", ep.id), - vendor_provided_label = name - } - ) - end - end - end - end - end -end - - local zigbee_switch_driver_template = { supported_capabilities = { capabilities.switch, @@ -186,9 +130,9 @@ local zigbee_switch_driver_template = { current_config_version = 1, lifecycle_handlers = { init = configurationMap.power_reconfig_wrapper(device_init), - added = device_added, - infoChanged = info_changed, - doConfigure = do_configure + added = lazy_handler("lifecycle_handlers.device_added"), + infoChanged = lazy_handler("lifecycle_handlers.info_changed"), + doConfigure = lazy_handler("lifecycle_handlers.do_configure"), }, health_check = false, } diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua new file mode 100644 index 0000000000..44b2979c33 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua @@ -0,0 +1,56 @@ +-- Copyright 2025 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 find_child = require "lifecycle_handlers.find_child" + +local function is_mcd_device(device) + local components = device.profile.components + if type(components) == "table" then + local component_count = 0 + for _, component in pairs(components) do + component_count = component_count + 1 + end + return component_count >= 2 + end +end + +return function(driver, device, event) + local clusters = require "st.zigbee.zcl.clusters" + local device_lib = require "st.device" + + local main_endpoint = device:get_endpoint(clusters.OnOff.ID) + if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then + for _, ep in ipairs(device.zigbee_endpoints) do + if ep.id ~= main_endpoint then + if device:supports_server_cluster(clusters.OnOff.ID, ep.id) then + device:set_find_child(find_child) + if find_child(device, ep.id) == nil then + local name = string.format("%s %d", device.label, ep.id) + local child_profile = "basic-switch" + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%02X", ep.id), + vendor_provided_label = name + } + ) + end + end + end + end + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua new file mode 100644 index 0000000000..5de558aedc --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua @@ -0,0 +1,27 @@ +-- Copyright 2025 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" +return function(self, device) + device:refresh() + device:configure() + + -- Additional one time configuration + if device:supports_capability(capabilities.energyMeter) or device:supports_capability(capabilities.powerMeter) then + local clusters = require "st.zigbee.zcl.clusters" + -- Divisor and multipler for EnergyMeter + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua new file mode 100644 index 0000000000..e33df67817 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 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. + +return function(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) +end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua new file mode 100644 index 0000000000..9699760a92 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 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. + +return function(self, device, event, args) + local preferences = require "preferences" + preferences.update_preferences(self, device, args) +end From 8c40cc2448aa758ef42b38129aea3811a6d16a36 Mon Sep 17 00:00:00 2001 From: thinkaName <962679819@qq.com> Date: Thu, 16 Oct 2025 11:00:32 +0800 Subject: [PATCH 195/449] add screen VIVIDSTORM VWSDSTUST120H --- .../zigbee-window-treatment/fingerprints.yml | 5 + .../projector-screen-VWSDSTUST120H.yml | 23 ++ .../src/VIVIDSTORM/custom_clusters.lua | 34 ++ .../src/VIVIDSTORM/init.lua | 162 +++++++++ .../zigbee-window-treatment/src/init.lua | 3 +- ..._zigbee_window_treatment_VWSDSTUST120H.lua | 327 ++++++++++++++++++ tools/localizations/cn.csv | 1 + 7 files changed, 554 insertions(+), 1 deletion(-) create mode 100755 drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index f77f2db946..808add60ff 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -123,6 +123,11 @@ zigbeeManufacturer: manufacturer: Shade Revolution model: Indoor Shade Motors deviceProfileName: window-treatment-powerSource + - id: "VIVIDSTORM/VWSDSTUST120H" + deviceLabel: VIVIDSTORM Smart Screen VWSDSTUST120H + manufacturer: VIVIDSTORM + model: VWSDSTUST120H + deviceProfileName: projector-screen-VWSDSTUST120H zigbeeGeneric: - id: "genericShade" diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml new file mode 100755 index 0000000000..85519e9f31 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml @@ -0,0 +1,23 @@ +name: projector-screen-VWSDSTUST120H +components: + - label: " " + id: main + capabilities: + - id: windowShade + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Projector + - label: " " + id: hardwareFault + capabilities: + - id: hardwareFault + version: 1 +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-VIVIDSTORM_Projector_Screen diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua new file mode 100755 index 0000000000..6f266a474e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua @@ -0,0 +1,34 @@ +-- Copyright 2025 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 data_types = require "st.zigbee.data_types" + +local custom_clusters = { + motor = { + id = 0xFCC9, + mfg_specific_code = 0x1235, + attributes = { + mode_value = { + id = 0x0000, + value_type = data_types.Uint8, + }, + hardwareFault = { + id = 0x0001, + value_type = data_types.Uint8, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua new file mode 100755 index 0000000000..ef36757490 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua @@ -0,0 +1,162 @@ +-- Copyright 2025 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 zcl_clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local custom_clusters = require "VIVIDSTORM/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" +local WindowCovering = zcl_clusters.WindowCovering + +local MOST_RECENT_SETLEVEL = "windowShade_recent_setlevel" +local TIMER = "liftPercentage_timer" + + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } +} + +local is_zigbee_window_shade = function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local mode_str = { "Delete upper limit","Set the upper limit","Delete lower limit","Set the lower limit" } + +local function mode_attr_handler(driver, device, value, zb_rx) + if value.value <= 3 then + local value = mode_str[value.value+1] + if value ~= nil then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode(value)) + end + end +end + + +local function liftPercentage_attr_handler(driver, device, value, zb_rx) + local windowShade = capabilities.windowShade.windowShade + local components = device.profile.components.main + local most_recent_setlevel = device:get_field(MOST_RECENT_SETLEVEL) + if value.value and most_recent_setlevel and value.value ~= most_recent_setlevel then + if value.value > most_recent_setlevel then + device:emit_component_event(components,windowShade.opening()) + elseif value.value < most_recent_setlevel then + device:emit_component_event(components,windowShade.closing()) + end + end + device:set_field(MOST_RECENT_SETLEVEL, value.value) + + local timer = device:get_field(TIMER) + if timer ~= nil then driver:cancel_timer(timer) end + timer = device.thread:call_with_delay(5, function(d) + if most_recent_setlevel == 0 then + device:emit_component_event(components,windowShade.closed()) + elseif most_recent_setlevel == 100 then + device:emit_component_event(components,windowShade.open()) + else + device:emit_component_event(components,windowShade.partially_open()) + end + end + ) + device:set_field(TIMER, timer) +end + +local function hardwareFault_attr_handler(driver, device, value, zb_rx) + if value.value == 1 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.detected()) + elseif value.value == 0 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + end +end + +local function capabilities_mode_handler(driver, device, command) + local value = 0 + if command.args.mode == "Delete upper limit" then + value = 0 + elseif command.args.mode == "Set the upper limit" then + value = 1 + elseif command.args.mode == "Delete lower limit" then + value = 2 + elseif command.args.mode == "Set the lower limit" then + value = 3 + end + + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + value + ) + ) +end + +local function do_refresh(driver, device) + device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.mode_value) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.hardwareFault) +end + +local function added_handler(self, device) + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + do_refresh(self, device) +end + +local screen_handler = { + NAME = "VWSDSTUST120H Device Handler", + supported_capabilities = { + capabilities.refresh + }, + lifecycle_handlers = { + added = added_handler + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capabilities_mode_handler + }, + }, + zigbee_handlers = { + attr = { + [WindowCovering.ID] = { + [WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = liftPercentage_attr_handler + }, + [custom_clusters.motor.id] = { + [custom_clusters.motor.attributes.mode_value.id] = mode_attr_handler, + [custom_clusters.motor.attributes.hardwareFault.id] = hardwareFault_attr_handler + } + } + }, + can_handle = is_zigbee_window_shade, +} + +return screen_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 933e0d6977..246d65f463 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -56,7 +56,8 @@ local zigbee_window_treatment_driver_template = { require("axis"), require("yoolax"), require("hanssem"), - require("screen-innovations")}, + require("screen-innovations"), + require("VIVIDSTORM")}, lifecycle_handlers = { init = init_handler, added = added_handler 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 new file mode 100755 index 0000000000..394498b3c8 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua @@ -0,0 +1,327 @@ +-- Copyright 2025 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 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 data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PRIVATE_CLUSTER_ID = 0xFCC9 +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("projector-screen-VWSDSTUST120H.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "VIVIDSTORM", + model = "VWSDSTUST120H", + server_clusters = {0x0000, 0x0102, 0xFCC9} + } + } + } +) + +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( + "capability - refresh", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + local read_0x0001_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", capabilities.hardwareFault.hardwareFault.clear())) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + local read_0x0001_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.WindowCovering.server.commands.UpOrOpen(mock_device) } + } + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.DownOrClose(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_coroutine_test( + "Handle Setlimit Delete upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 1) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Delete lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 2) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 3) + }) + end +) + +test.register_coroutine_test( + "Device reported mode 0 and driver emit Delete upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Delete upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 1 and driver emit Set the upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Set the upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 2 and driver emit Delete lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Delete lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 3 and driver emit Set the lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.mode.mode("Set the lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 0 and driver emit capabilities.hardwareFault.hardwareFault.clear()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", + capabilities.hardwareFault.hardwareFault.clear())) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 1 and driver emit capabilities.hardwareFault.hardwareFault.detected()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", + capabilities.hardwareFault.hardwareFault.detected())) + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 5 emit closing", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 5) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 0 emit closed", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index ed47575149..d9105f0949 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -116,3 +116,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 +"VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H From 49050c47e50b1ac8e19837b72158ca5813c0b656 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 16 Oct 2025 11:48:47 -0700 Subject: [PATCH 196/449] WWSTCERT-8293 Meross Smart Wi-Fi Thermostat --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 4865bb00db..8358e3164e 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -44,6 +44,12 @@ matterManufacturer: vendorId: 0x1206 productId: 0x0001 deviceProfileName: thermostat-nostate-nobattery + #Meross + - id: "4933/57345" + deviceLabel: Smart Wi-Fi Thermostat + vendorId: 0x1345 + productId: 0xE001 + deviceProfileName: thermostat-fan-nostate-nobattery #Siterwell - id: "4736/769" deviceLabel: Siterwell Radiator Thermostat From a84be118cea9c681287933024ce5591621b0d9f9 Mon Sep 17 00:00:00 2001 From: wkhenon Date: Thu, 25 Sep 2025 07:34:04 -0400 Subject: [PATCH 197/449] Use native & default handlers for ZLL devices --- .../ikea-xy-color-bulb/init.lua | 8 +++++++- drivers/SmartThings/zigbee-switch/src/init.lua | 4 ++-- .../src/lifecycle_handlers/device_added.lua | 5 +++++ .../src/lifecycle_handlers/do_configure.lua | 6 +++++- ...st_sengled_dimmer_bulb_with_motion_sensor.lua | 16 ++++++---------- .../src/test/test_zll_color_temp_bulb.lua | 5 +++++ .../src/test/test_zll_dimmer_bulb.lua | 4 ++++ .../zigbee-switch/src/test/test_zll_rgb_bulb.lua | 4 ++++ .../src/test/test_zll_rgbw_bulb.lua | 5 +++++ .../src/zigbee-dimming-light/init.lua | 8 +++++++- .../zigbee-switch/src/zll-dimmer-bulb/init.lua | 3 --- .../zigbee-switch/src/zll-polling/init.lua | 4 ++-- 12 files changed, 52 insertions(+), 20 deletions(-) rename drivers/SmartThings/zigbee-switch/src/{zll-dimmer-bulb => }/ikea-xy-color-bulb/init.lua (96%) diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua similarity index 96% rename from drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua rename to drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua index ed6a24ba61..b7080ad446 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua @@ -35,7 +35,13 @@ local IKEA_XY_COLOR_BULB_FINGERPRINTS = { } local function can_handle_ikea_xy_color_bulb(opts, driver, device) - return (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false + local can_handle = (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("ikea-xy-color-bulb") + return true, subdriver + else + return false + end end local device_init = function(self, device) diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 81c39297e3..28beb014f4 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -31,7 +31,6 @@ end local function lazy_load_if_possible(sub_driver_name) -- gets the current lua libs api version - -- version 9 will include the lazy loading functions if version.api >= 9 then return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) @@ -105,7 +104,8 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("zigbee-dimming-light"), lazy_load_if_possible("white-color-temp-bulb"), lazy_load_if_possible("rgbw-bulb"), - lazy_load_if_possible("zll-dimmer-bulb"), + (version.api < 16) and lazy_load_if_possible("zll-dimmer-bulb") or nil, + lazy_load_if_possible("ikea-xy-color-bulb"), lazy_load_if_possible("zll-polling"), lazy_load_if_possible("zigbee-switch-power"), lazy_load_if_possible("ge-link-bulb"), diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua index 44b2979c33..275b7e4b27 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua @@ -27,7 +27,9 @@ end return function(driver, device, event) local clusters = require "st.zigbee.zcl.clusters" + local ZLL_PROFILE_ID = 0xC05E local device_lib = require "st.device" + local version = require "version" local main_endpoint = device:get_endpoint(clusters.OnOff.ID) if is_mcd_device(device) == false and device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then @@ -53,4 +55,7 @@ return function(driver, device, event) end end end + if version.api > 15 and device:get_profile_id() == ZLL_PROFILE_ID then + device:refresh() + end end diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua index 5de558aedc..088cc604b4 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua @@ -14,7 +14,11 @@ local capabilities = require "st.capabilities" return function(self, device) - device:refresh() + local ZLL_PROFILE_ID = 0xC05E + local version = require "version" + if version.api < 16 or (version.api > 15 and device:get_profile_id() ~= ZLL_PROFILE_ID) then + device:refresh() + end device:configure() -- Additional one time configuration 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 6051a9decd..2ccaf7be36 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 @@ -16,12 +16,12 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff local Level = clusters.Level local IASZone = clusters.IASZone -local IasEnrollResponseCode = IASZone.types.EnrollResponseCode local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("on-off-level-motion-sensor.yml"), @@ -31,7 +31,8 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "sengled", model = "E13-N11", - server_clusters = { 0x0006, 0x0008, 0x0500 } + server_clusters = { 0x0006, 0x0008, 0x0500 }, + profile_id = 0xC05E } } } @@ -76,14 +77,6 @@ test.register_coroutine_test( mock_device.id, IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 300, 1) }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) - }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -116,6 +109,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -134,6 +128,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -152,6 +147,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) 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 6d7986700e..fc68ec7786 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 @@ -15,6 +15,7 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -83,6 +84,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -98,6 +100,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -113,6 +116,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = {50} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.commands.MoveToLevelWithOnOff(mock_device, math.floor(50 / 100.0 * 254), 0xFFFF)}) test.wait_for_events() test.mock_time.advance_time(2) @@ -128,6 +132,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) test.wait_for_events() 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 922a0ce364..a82dac084a 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 @@ -15,6 +15,7 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -121,6 +122,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -138,6 +140,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -155,6 +158,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) 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 b58acb6dc2..7bf6b175cd 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 @@ -16,6 +16,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -149,6 +150,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -168,6 +170,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -187,6 +190,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) 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 51edc21777..7945b8d030 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 @@ -15,6 +15,7 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" +local version = require "version" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local OnOff = clusters.OnOff @@ -140,6 +141,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "on") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device) }) @@ -160,6 +162,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switch", component = "main", command = "off", args = {} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switch", "off") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.Off(mock_device) }) @@ -180,6 +183,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57 } } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("switchLevel", "setLevel") end test.socket.zigbee:__expect_send({ mock_device.id, Level.server.commands.MoveToLevelWithOnOff(mock_device, 144, 0xFFFF) }) @@ -200,6 +204,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.socket.capability:__queue_receive({ mock_device.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {200} } }) + if version.api > 15 then mock_device:expect_native_cmd_handler_registration("colorTemperature", "setColorTemperature") end test.socket.zigbee:__expect_send({ mock_device.id, OnOff.commands.On(mock_device)}) test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.commands.MoveToColorTemperature(mock_device, 5000, 0x0000)}) diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 28312a45e1..68e924a8f1 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -85,6 +85,11 @@ local function can_handle_zigbee_dimming_light(opts, driver, device) return false end +local function do_configure(driver, device) + device:refresh() + device:configure() +end + local function device_init(driver, device) for _,attribute in ipairs(DIMMING_LIGHT_CONFIGURATION) do device:add_configured_attribute(attribute) @@ -99,7 +104,8 @@ local zigbee_dimming_light = { NAME = "Zigbee Dimming Light", lifecycle_handlers = { init = configurations.power_reconfig_wrapper(device_init), - added = device_added + added = device_added, + doConfigure = do_configure }, sub_drivers = { require("zigbee-dimming-light/osram-iqbr30"), diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua index 6c74f9b89f..0f35666fe0 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua @@ -204,9 +204,6 @@ local zll_dimmer_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature } }, - sub_drivers = { - require("zll-dimmer-bulb/ikea-xy-color-bulb") - }, can_handle = can_handle_zll_dimmer_bulb } diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index d3ce34c2fd..1fa972ca98 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -13,12 +13,12 @@ -- limitations under the License. local device_lib = require "st.device" -local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" +local ZLL_PROFILE_ID = 0xC05E local function zll_profile(opts, driver, device, zb_rx, ...) local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] - if (endpoint ~= nil and endpoint.profile_id == constants.ZLL_PROFILE_ID) then + if (endpoint ~= nil and endpoint.profile_id == ZLL_PROFILE_ID) then local subdriver = require("zll-polling") return true, subdriver else From 658810eee588913ed7017fbf2768059a359bf00e Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Fri, 17 Oct 2025 11:23:53 +0900 Subject: [PATCH 198/449] Support robotCleanerOperatingState Routine (#2443) Signed-off-by: Hunsup Jung --- .../src/RvcOperationalState/init.lua | 2 + .../server/attributes/AcceptedCommandList.lua | 75 ++++++++++++++++++ .../server/commands/GoHome.lua | 79 +++++++++++++++++++ drivers/SmartThings/matter-rvc/src/init.lua | 68 ++++++++++------ .../matter-rvc/src/test/test_matter_rvc.lua | 26 ++++-- 5 files changed, 220 insertions(+), 30 deletions(-) create mode 100644 drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua create mode 100644 drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua index b58f105694..3be13eaef6 100644 --- a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/init.lua @@ -39,6 +39,7 @@ function RvcOperationalState:get_server_command_by_id(command_id) [0x0001] = "Stop", [0x0002] = "Start", [0x0003] = "Resume", + [0x0080] = "GoHome", } if server_id_map[command_id] ~= nil then return self.server.commands[server_id_map[command_id]] @@ -74,6 +75,7 @@ RvcOperationalState.command_direction_map = { ["Stop"] = "server", ["Start"] = "server", ["Resume"] = "server", + ["GoHome"] = "server", ["OperationalCommandResponse"] = "client", } diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua new file mode 100644 index 0000000000..dd92f54543 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/attributes/AcceptedCommandList.lua @@ -0,0 +1,75 @@ +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local AcceptedCommandList = { + ID = 0xFFF9, + NAME = "AcceptedCommandList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint32", +} + +function AcceptedCommandList: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, AcceptedCommandList.element_type) + end +end + +function AcceptedCommandList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AcceptedCommandList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil --event_id + ) +end + +function AcceptedCommandList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AcceptedCommandList: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 AcceptedCommandList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) +return AcceptedCommandList + diff --git a/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua new file mode 100644 index 0000000000..4328914a1a --- /dev/null +++ b/drivers/SmartThings/matter-rvc/src/RvcOperationalState/server/commands/GoHome.lua @@ -0,0 +1,79 @@ +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local GoHome = {} + +GoHome.NAME = "GoHome" +GoHome.ID = 0x0080 +GoHome.field_defs = { +} + +function GoHome:init(device, endpoint_id) + local out = {} + local args = {} + if #args > #self.field_defs then + error(self.NAME .. " received too many arguments") + end + for i,v in ipairs(self.field_defs) do + if v.is_optional and args[i] == nil then + out[v.name] = nil + elseif v.is_nullable and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(args[i], data_types.Null, v.name) + out[v.name].field_id = v.field_id + elseif not v.is_optional and args[i] == nil then + out[v.name] = data_types.validate_or_build_type(v.default, v.data_type, v.name) + out[v.name].field_id = v.field_id + else + out[v.name] = data_types.validate_or_build_type(args[i], v.data_type, v.name) + out[v.name].field_id = v.field_id + end + end + setmetatable(out, { + __index = GoHome, + __tostring = GoHome.pretty_print + }) + return self._cluster:build_cluster_command( + device, + out, + endpoint_id, + self._cluster.ID, + self.ID + ) +end + +function GoHome:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function GoHome:augment_type(base_type_obj) + local elems = {} + for _, v in ipairs(base_type_obj.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) + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + 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 + base_type_obj.elements = elems +end + +function GoHome:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(GoHome, {__call = GoHome.init}) + +return GoHome diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 25a202241a..1e52f321ee 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -35,9 +35,21 @@ end local RUN_MODE_SUPPORTED_MODES = "__run_mode_supported_modes" local CURRENT_RUN_MODE = "__current_run_mode" local CLEAN_MODE_SUPPORTED_MODES = "__clean_mode_supported_modes" -local OPERATING_STATE_SUPPORTED_COMMANDS = "__operating_state_supported_commands" local SERVICE_AREA_PROFILED = "__SERVICE_AREA_PROFILED" +local clus_op_enum = clusters.OperationalState.types.OperationalStateEnum +local clus_rvc_op_enum = clusters.RvcOperationalState.types.OperationalStateEnum +local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState +local cap_op_cmds = capabilities.robotCleanerOperatingState.commands +local OPERATING_STATE_MAP = { + [clus_op_enum.STOPPED] = cap_op_enum.stopped, + [clus_op_enum.RUNNING] = cap_op_enum.running, + [clus_op_enum.PAUSED] = cap_op_enum.paused, + [clus_rvc_op_enum.SEEKING_CHARGER] = cap_op_enum.seekingCharger, + [clus_rvc_op_enum.CHARGING] = cap_op_enum.charging, + [clus_rvc_op_enum.DOCKED] = cap_op_enum.docked +} + local subscribed_attributes = { [capabilities.mode.ID] = { clusters.RvcRunMode.attributes.SupportedModes, @@ -46,6 +58,7 @@ local subscribed_attributes = { clusters.RvcCleanMode.attributes.CurrentMode }, [capabilities.robotCleanerOperatingState.ID] = { + clusters.RvcOperationalState.attributes.OperationalStateList, clusters.RvcOperationalState.attributes.OperationalState, clusters.RvcOperationalState.attributes.OperationalError }, @@ -103,6 +116,12 @@ local function do_configure(driver, device) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) end +local function driver_switched(driver, device) + match_profile(driver, device) + device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) + device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) +end + local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then device:subscribe() @@ -111,7 +130,11 @@ end -- Helper functions -- local function supports_rvc_operational_state(device, command_name) - local supported_op_commands = device:get_field(OPERATING_STATE_SUPPORTED_COMMANDS) or {} + local supported_op_commands = device:get_latest_state( + "main", + capabilities.robotCleanerOperatingState.ID, + capabilities.robotCleanerOperatingState.supportedCommands.NAME + ) or {} for _, cmd in ipairs(supported_op_commands) do if cmd == command_name then return true @@ -127,8 +150,6 @@ local function can_send_state_command(device, command_name, current_state, curre end local set_mode = capabilities.mode.commands.setMode.NAME - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if command_name ~= set_mode and supports_rvc_operational_state(device, command_name) == false then return false end @@ -196,9 +217,7 @@ local function update_supported_arguments(device, ep, current_run_mode, current_ end -- Set Supported Operating State Commands - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands local supported_op_commands = {} - if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, nil) == true then table.insert(supported_op_commands, cap_op_cmds.goHome.NAME) end @@ -312,17 +331,6 @@ end local function rvc_operational_state_attr_handler(driver, device, ib, response) device.log.info(string.format("rvc_operational_state_attr_handler operationalState: %s", ib.data.value)) - local clus_op_enum = clusters.OperationalState.types.OperationalStateEnum - local clus_rvc_op_enum = clusters.RvcOperationalState.types.OperationalStateEnum - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState - local OPERATING_STATE_MAP = { - [clus_op_enum.STOPPED] = cap_op_enum.stopped, - [clus_op_enum.RUNNING] = cap_op_enum.running, - [clus_op_enum.PAUSED] = cap_op_enum.paused, - [clus_rvc_op_enum.SEEKING_CHARGER] = cap_op_enum.seekingCharger, - [clus_rvc_op_enum.CHARGING] = cap_op_enum.charging, - [clus_rvc_op_enum.DOCKED] = cap_op_enum.docked - } if ib.data.value ~= clus_op_enum.ERROR then device:emit_event_for_endpoint(ib.endpoint_id, OPERATING_STATE_MAP[ib.data.value]()) end @@ -369,9 +377,21 @@ local function rvc_operational_error_attr_handler(driver, device, ib, response) end end +local function rvc_operational_state_list_attr_handler(driver, device, ib, response) + local supportedOperatingState = {} + for _, state in ipairs(ib.data.elements) do + clusters.RvcOperationalState.types.OperationalStateStruct:augment_type(state) + if OPERATING_STATE_MAP[state.elements.operational_state_id.value] ~= nil then + table.insert(supportedOperatingState, OPERATING_STATE_MAP[state.elements.operational_state_id.value].NAME) + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.robotCleanerOperatingState.supportedOperatingStates( + supportedOperatingState, {visibility = {displayed = false}} + )) +end + local function handle_rvc_operational_state_accepted_command_list(driver, device, ib, response) device.log.info("handle_rvc_operational_state_accepted_command_list") - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands local OP_COMMAND_MAP = { [clusters.RvcOperationalState.commands.Pause.ID] = cap_op_cmds.pause, [clusters.RvcOperationalState.commands.Resume.ID] = cap_op_cmds.start, @@ -381,7 +401,9 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device for _, attr in ipairs(ib.data.elements) do table.insert(supportedOperatingStateCommands, OP_COMMAND_MAP[attr.value].NAME) end - device:set_field(OPERATING_STATE_SUPPORTED_COMMANDS, supportedOperatingStateCommands, { persist = true }) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.robotCleanerOperatingState.supportedCommands( + supportedOperatingStateCommands, {visibility = {displayed = false}} + )) -- Get current run mode, current tag, current operating state local current_run_mode = device:get_field(CURRENT_RUN_MODE) @@ -398,7 +420,6 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if current_state ~= cap_op_enum.stopped.NAME and current_state ~= cap_op_enum.running.NAME and current_state ~= cap_op_enum.paused.NAME and current_state ~= cap_op_enum.seekingCharger.NAME and current_state ~= cap_op_enum.charging.NAME and current_state ~= cap_op_enum.docked.NAME then @@ -406,7 +427,6 @@ local function handle_rvc_operational_state_accepted_command_list(driver, device end -- Set Supported Operating State Commands - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands local supported_op_commands = {} if can_send_state_command(device, cap_op_cmds.goHome.NAME, current_state, current_tag) == true then table.insert(supported_op_commands, cap_op_cmds.goHome.NAME) @@ -482,7 +502,7 @@ local function rvc_service_area_selected_areas_handler(driver, device, ib, respo capabilities.serviceArea.ID, capabilities.serviceArea.supportedAreas.NAME ) - for i, area in ipairs(supported_areas) do + for i, area in ipairs(supported_areas or {}) do table.insert(selected_areas, area.areaId) end end @@ -528,14 +548,12 @@ local function handle_robot_cleaner_operating_state_start(driver, device, cmd) capabilities.robotCleanerOperatingState.ID, capabilities.robotCleanerOperatingState.operatingState.NAME ) - local cap_op_enum = capabilities.robotCleanerOperatingState.operatingState if current_state ~= cap_op_enum.stopped.NAME and current_state ~= cap_op_enum.running.NAME and current_state ~= cap_op_enum.paused.NAME and current_state ~= cap_op_enum.seekingCharger.NAME and current_state ~= cap_op_enum.charging.NAME and current_state ~= cap_op_enum.docked.NAME then current_state = "Error" end - local cap_op_cmds = capabilities.robotCleanerOperatingState.commands if can_send_state_command(device, cap_op_cmds.start.NAME, current_state, current_tag) == true then device:send(clusters.RvcOperationalState.commands.Resume(device, endpoint_id)) elseif can_send_state_command(device, capabilities.mode.commands.setMode.NAME, current_state, current_tag) == true then @@ -594,6 +612,7 @@ local matter_rvc_driver = { init = device_init, doConfigure = do_configure, infoChanged = info_changed, + driverSwitched = driver_switched }, matter_handlers = { attr = { @@ -606,6 +625,7 @@ local matter_rvc_driver = { [clusters.RvcCleanMode.attributes.CurrentMode.ID] = clean_mode_current_mode_handler, }, [clusters.RvcOperationalState.ID] = { + [clusters.RvcOperationalState.attributes.OperationalStateList.ID] = rvc_operational_state_list_attr_handler, [clusters.RvcOperationalState.attributes.OperationalState.ID] = rvc_operational_state_attr_handler, [clusters.RvcOperationalState.attributes.OperationalError.ID] = rvc_operational_error_attr_handler, [clusters.RvcOperationalState.attributes.AcceptedCommandList.ID] = handle_rvc_operational_state_accepted_command_list, 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 c051f188fb..f3edc6ac20 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -70,14 +70,15 @@ local function test_init() test.mock_device.add_test_device(mock_device) local subscribed_attributes = { [capabilities.mode.ID] = { - clusters.RvcRunMode.attributes.SupportedModes, - clusters.RvcRunMode.attributes.CurrentMode, - clusters.RvcCleanMode.attributes.SupportedModes, - clusters.RvcCleanMode.attributes.CurrentMode, + clusters.RvcRunMode.attributes.SupportedModes, + clusters.RvcRunMode.attributes.CurrentMode, + clusters.RvcCleanMode.attributes.SupportedModes, + clusters.RvcCleanMode.attributes.CurrentMode, }, [capabilities.robotCleanerOperatingState.ID] = { - clusters.RvcOperationalState.attributes.OperationalState, - clusters.RvcOperationalState.attributes.OperationalError + clusters.RvcOperationalState.attributes.OperationalStateList, + clusters.RvcOperationalState.attributes.OperationalState, + clusters.RvcOperationalState.attributes.OperationalError }, [capabilities.serviceArea.ID] = { clusters.ServiceArea.attributes.SupportedAreas, @@ -192,6 +193,19 @@ local function operating_state_init() SUPPORTED_OPERATIONAL_STATE_COMMAND ) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.robotCleanerOperatingState.supportedCommands( + { + capabilities.robotCleanerOperatingState.commands.pause.NAME, + capabilities.robotCleanerOperatingState.commands.start.NAME, + capabilities.robotCleanerOperatingState.commands.goHome.NAME + }, + {visibility = {displayed = false}} + ) + ) + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", From 8cadf183fd1c3b43a04647c5a59e701a98f8c8b7 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 17 Oct 2025 14:43:32 -0700 Subject: [PATCH 199/449] Revert WWSTCERT-7351 --- drivers/SmartThings/matter-switch/fingerprints.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 508e481144..b95258f7fe 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -438,13 +438,6 @@ matterManufacturer: productId: 0x0016 deviceProfileName: light-color-level-2000K-7000K -# Govee - - id: "4999/24584" - deviceLabel: Govee Smart Bulb - vendorId: 0x1387 - productId: 0x6008 - deviceProfileName: light-color-level-2700K-6500K - # Hue - id: "4107/2049" deviceLabel: Hue W 1600 A21 E26 1P NAM From 784b0d6a96ecf3b17981e79438580aa04f6618c1 Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:51:29 +0200 Subject: [PATCH 200/449] WWSTCERT-8138: Add support for frient Zigbee Range Extender (#2430) * Add support * Delete ZCLVersion reading when refreshing from frient subdriver --- .../zigbee-range-extender/fingerprints.yml | 5 + .../range-extender-battery-source.yml | 20 ++ .../zigbee-range-extender/src/frient/init.lua | 78 ++++++++ .../zigbee-range-extender/src/init.lua | 11 +- .../test_frient_zigbee_range_extender.lua | 188 ++++++++++++++++++ 5 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml create mode 100644 drivers/SmartThings/zigbee-range-extender/src/frient/init.lua create mode 100644 drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua diff --git a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml index e889fc1839..d8d4bcd5cf 100644 --- a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml +++ b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml @@ -29,3 +29,8 @@ zigbeeManufacturer: manufacturer: Insta GmbH model: NEXENTRO Pushbutton Interface deviceProfileName: range-extender + - id: "frientA/S/111" + deviceLabel: frient Zigbee Range Extender + manufacturer: frient A/S + model: REXZB-111 + deviceProfileName: range-extender-battery-source \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml new file mode 100644 index 0000000000..f8958a3901 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml @@ -0,0 +1,20 @@ +name: range-extender-battery-source +components: + - id: main + capabilities: + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + config: + values: + - key: "powerSource.value" + enabledValues: + - battery + - mains + categories: + - name: Networking diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua new file mode 100644 index 0000000000..f4a754bfeb --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua @@ -0,0 +1,78 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration + +local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) + device:emit_event_for_endpoint( + zigbee_message.address_header.src_endpoint.value, + zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains() + ) +end + +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + +local function device_added(driver, device) + device:emit_event(capabilities.powerSource.powerSource.mains()) +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device) +end + +local function do_refresh(driver, device) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + device:send(IASZone.attributes.ZoneStatus:read(device)) +end + +local frient_range_extender = { + NAME = "frient Range Extender", + lifecycle_handlers = { + added = device_added, + init = device_init + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + zigbee_handlers = { + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + }, + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + } + }, + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111") + end +} + +return frient_range_extender \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index a8c4a2796a..2523a3d45e 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -13,7 +13,7 @@ -- limitations under the License. local capabilities = require "st.capabilities" - +local defaults = require "st.zigbee.defaults" local Basic = (require "st.zigbee.zcl.clusters").Basic local ZigbeeDriver = require "st.zigbee" @@ -23,7 +23,8 @@ end local zigbee_range_driver_template = { supported_capabilities = { - capabilities.refresh + capabilities.refresh, + capabilities.battery }, capability_handlers = { [capabilities.refresh.ID] = { @@ -31,8 +32,13 @@ local zigbee_range_driver_template = { } }, health_check = false, + sub_drivers = { + require("frient") + } } +defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities) + local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbee_range_driver_template) function zigbee_range_extender_driver:device_health_check() @@ -42,6 +48,7 @@ function zigbee_range_extender_driver:device_health_check() device:send(Basic.attributes.ZCLVersion:read(device)) end end + zigbee_range_extender_driver.device_health_timer = zigbee_range_extender_driver.call_on_schedule(zigbee_range_extender_driver, 300, zigbee_range_extender_driver.device_health_check) zigbee_range_extender_driver:run() 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 new file mode 100644 index 0000000000..8f63df1705 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua @@ -0,0 +1,188 @@ +-- Copyright 2025 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 t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("range-extender-battery-source.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "REXZB-111", + server_clusters = {IASZone.ID, PowerConfiguration.ID } + } + } + } +) + +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_coroutine_test( + "Refresh necessary 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, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + end +) + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read( mock_device ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:read( mock_device ) + }) + + 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.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "Power source / battery should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + } + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 33) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 37) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 41) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.run_registered_tests() From 6e241f1e945427237d07424a59122b89e2822804 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 17 Oct 2025 17:01:42 -0500 Subject: [PATCH 201/449] include capabilities file for test cases --- .../robotCleanerOperatingState.yml | 108 ++++++++++++++++++ .../matter-rvc/src/test/test_matter_rvc.lua | 1 + 2 files changed, 109 insertions(+) create mode 100644 drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml diff --git a/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml b/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml new file mode 100644 index 0000000000..d6cea7f3f2 --- /dev/null +++ b/drivers/SmartThings/matter-rvc/capabilities/robotCleanerOperatingState.yml @@ -0,0 +1,108 @@ +name: Robot Cleaner Operating State +status: live +attributes: + operatingState: + schema: + type: object + additionalProperties: false + properties: + value: + type: string + enum: + - stopped + - running + - paused + - seekingCharger + - charging + - docked + - unableToStartOrResume + - unableToCompleteOperation + - commandInvalidInState + - failedToFindChargingDock + - stuck + - dustBinMissing + - dustBinFull + - waterTankEmpty + - waterTankMissing + - waterTankLidOpen + - mopCleaningPadMissing + required: + - value + enumCommands: + - command: start + value: running + - command: pause + value: paused + - command: goHome + value: seekingCharger + actedOnBy: + - start + - pause + - goHome + supportedOperatingStates: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - stopped + - running + - paused + - seekingCharger + - charging + - docked + - unableToStartOrResume + - unableToCompleteOperation + - commandInvalidInState + - failedToFindChargingDock + - stuck + - dustBinMissing + - dustBinFull + - waterTankEmpty + - waterTankMissing + - waterTankLidOpen + - mopCleaningPadMissing + required: + - value + supportedCommands: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - start + - pause + - goHome + supportedOperatingStateCommands: + schema: + type: object + additionalProperties: false + properties: + value: + type: array + items: + type: string + enum: + - start + - pause + - goHome +commands: + start: + arguments: [] + name: start + pause: + arguments: [] + name: pause + goHome: + arguments: [] + name: goHome +id: robotCleanerOperatingState +version: 1 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 f3edc6ac20..e8d91fd1ca 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -17,6 +17,7 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" local version = require "version" +test.add_package_capability("robotCleanerOperatingState.yml") if version.api < 10 then clusters.RvcCleanMode = require "RvcCleanMode" From f7b0a86168602e46a23ee399d5f9a2819a0c9a5a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 20 Oct 2025 15:30:04 -0700 Subject: [PATCH 202/449] WWSTCERT-8285 NodOn Zigbee Temperature and Humidity Sensor --- .../SmartThings/zigbee-humidity-sensor/fingerprints.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index 9ab5af08d6..5096f9cb25 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -88,13 +88,18 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RSM0147Z deviceProfileName: humidity-temp-battery + - id: "NodOn/STPH-4-1-00" + deviceLabel: Zigbee Temperature and Humidity Sensor + manufacturer: NodOn + model: STPH-4-1-00 + deviceProfileName: humidity-temp-battery zigbeeGeneric: - id: "HumidityTempGeneric" deviceLabel: Multipurpose Sensor deviceIdentifiers: - 0x0302 - clusters: - server: + clusters: + server: - 0x0402 # Temperature Measurement Cluster - 0x0405 # Relative Humidity Measurement Cluster deviceProfileName: humidity-temperature From d1346eb3ec4523ebac5c800506a0bf8616619f20 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Tue, 21 Oct 2025 16:43:25 -0500 Subject: [PATCH 203/449] remove unused embedded data --- .../ElectricalEnergyMeasurement/init.lua | 14 -- .../types/Feature.lua | 88 ----------- .../ElectricalPowerMeasurement/init.lua | 53 ------- .../server/attributes/ActivePower.lua | 1 - .../types/Feature.lua | 138 ------------------ .../ElectricalPowerMeasurement/types/init.lua | 15 -- .../ValveConfigurationAndControl/init.lua | 42 ------ .../types/Feature.lua | 51 ------- .../src/test/test_aqara_light_switch_h2.lua | 11 ++ .../src/test/test_matter_water_valve.lua | 5 + 10 files changed, 16 insertions(+), 402 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua delete mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index f2a812055b..8c96de0563 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -12,15 +12,8 @@ ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "Accuracy", [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -30,15 +23,8 @@ function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) end ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua index 717ba6a2f3..7b9bc03714 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -9,75 +9,6 @@ Feature.EXPORTED_ENERGY = 0x0002 Feature.CUMULATIVE_ENERGY = 0x0004 Feature.PERIODIC_ENERGY = 0x0008 -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - function Feature.bits_are_valid(feature) local max = Feature.IMPORTED_ENERGY | @@ -91,25 +22,6 @@ function Feature.bits_are_valid(feature) end end -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - setmetatable(Feature, new_mt) return Feature diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua index c9f401dd5c..222c80090b 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -1,6 +1,5 @@ local cluster_base = require "st.matter.cluster_base" local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "embedded_clusters.ElectricalPowerMeasurement.types" local ElectricalPowerMeasurement = {} @@ -9,32 +8,10 @@ ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" ElectricalPowerMeasurement.server = {} ElectricalPowerMeasurement.client = {} ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -44,39 +21,9 @@ function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) end ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - local attribute_helper_mt = {} attribute_helper_mt.__index = function(self, key) local direction = ElectricalPowerMeasurement.attribute_direction_map[key] diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua index 6c34abd2f4..457f6484af 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -65,4 +65,3 @@ end setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) return ActivePower - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,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.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 8016a487c3..0000000000 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,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.ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua index 0535e3267c..696c324a81 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua @@ -14,20 +14,8 @@ ValveConfigurationAndControl.types = ValveConfigurationAndControlTypes function ValveConfigurationAndControl:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "OpenDuration", - [0x0001] = "DefaultOpenDuration", - [0x0002] = "AutoCloseTime", - [0x0003] = "RemainingDuration", [0x0004] = "CurrentState", - [0x0005] = "TargetState", [0x0006] = "CurrentLevel", - [0x0007] = "TargetLevel", - [0x0008] = "DefaultOpenLevel", - [0x0009] = "ValveFault", - [0x000A] = "LevelStep", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -47,32 +35,9 @@ function ValveConfigurationAndControl:get_server_command_by_id(command_id) return nil end -function ValveConfigurationAndControl:get_event_by_id(event_id) - local event_id_map = { - [0x0000] = "ValveStateChanged", - [0x0001] = "ValveFault", - } - if event_id_map[event_id] ~= nil then - return self.server.events[event_id_map[event_id]] - end - return nil -end - ValveConfigurationAndControl.attribute_direction_map = { - ["OpenDuration"] = "server", - ["DefaultOpenDuration"] = "server", - ["AutoCloseTime"] = "server", - ["RemainingDuration"] = "server", ["CurrentState"] = "server", - ["TargetState"] = "server", ["CurrentLevel"] = "server", - ["TargetLevel"] = "server", - ["DefaultOpenLevel"] = "server", - ["ValveFault"] = "server", - ["LevelStep"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ValveConfigurationAndControl.command_direction_map = { @@ -111,13 +76,6 @@ end ValveConfigurationAndControl.commands = {} setmetatable(ValveConfigurationAndControl.commands, command_helper_mt) -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ValveConfigurationAndControl.server.events[key] -end -ValveConfigurationAndControl.events = {} -setmetatable(ValveConfigurationAndControl.events, event_helper_mt) - setmetatable(ValveConfigurationAndControl, {__index = cluster_base}) return ValveConfigurationAndControl diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua index 7433a99c9e..ea3ebeb7c1 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua @@ -7,44 +7,6 @@ Feature.BASE_MASK = 0xFFFF Feature.TIME_SYNC = 0x0001 Feature.LEVEL = 0x0002 -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - TIME_SYNC = 0x0001, - LEVEL = 0x0002, -} - -Feature.is_time_sync_set = function(self) - return (self.value & self.TIME_SYNC) ~= 0 -end - -Feature.set_time_sync = function(self) - if self.value ~= nil then - self.value = self.value | self.TIME_SYNC - else - self.value = self.TIME_SYNC - end -end - -Feature.unset_time_sync = function(self) - self.value = self.value & (~self.TIME_SYNC & self.BASE_MASK) -end - -Feature.is_level_set = function(self) - return (self.value & self.LEVEL) ~= 0 -end - -Feature.set_level = function(self) - if self.value ~= nil then - self.value = self.value | self.LEVEL - else - self.value = self.LEVEL - end -end - -Feature.unset_level = function(self) - self.value = self.value & (~self.LEVEL & self.BASE_MASK) -end - function Feature.bits_are_valid(feature) local max = Feature.TIME_SYNC | @@ -56,19 +18,6 @@ function Feature.bits_are_valid(feature) end end -Feature.mask_methods = { - is_time_sync_set = Feature.is_time_sync_set, - set_time_sync = Feature.set_time_sync, - unset_time_sync = Feature.unset_time_sync, - is_level_set = Feature.is_level_set, - set_level = Feature.set_level, - unset_level = Feature.unset_level, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - setmetatable(Feature, new_mt) return Feature 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 27fe47f11a..548ce67890 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 @@ -19,6 +19,17 @@ local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button +local version = require "version" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end local aqara_parent_ep = 4 local aqara_child1_ep = 1 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 d4ead24458..16f0ea3103 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 @@ -17,6 +17,11 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local version = require "version" + +if version.api < 11 then + clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("water-valve-level.yml"), From 5947e55dab4941ab6abe8bd1ed292c9e9a5b1994 Mon Sep 17 00:00:00 2001 From: Bang Keuckdo Date: Wed, 22 Oct 2025 15:06:12 +0900 Subject: [PATCH 204/449] Enhance code coverate for zigbee-temperature driver --- .../src/test/test_aqara_thermostat.lua | 100 ++++++++++++++++++ .../src/test/test_leviton_rc.lua | 32 ++++++ .../src/test/test_popp_thermostat.lua | 90 ++++++++++++++++ .../src/test/test_resideo_dt300st_m000.lua | 14 +++ .../test_stelpro_ki_zigbee_thermostat.lua | 48 +++++++++ .../src/test/test_stelpro_thermostat.lua | 35 ++++++ 6 files changed, 319 insertions(+) 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 4a1dc2057f..84f305d428 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -113,6 +113,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected())) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_ALARM_INFORMATION_ID, data_types.Uint32.ID, 0x00000000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.hardwareFault.hardwareFault.clear())) end ) @@ -128,6 +138,26 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationSuccess())) + + local attr_report_data_1 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationPending())) + + local attr_report_data_2 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationFailure())) end ) @@ -145,6 +175,18 @@ test.register_coroutine_test( capabilities.thermostatMode.thermostatMode.manual())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -162,6 +204,54 @@ test.register_coroutine_test( capabilities.valve.valve.closed())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint","stse.valveCalibration","thermostatMode","lock"}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.manual())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_2 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_3 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_3, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) + + local attr_report_data_4 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_4, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -177,6 +267,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked())) + + local attr_report_data_1 = { + { PRIVATE_CHILD_LOCK_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", + capabilities.lock.lock.locked())) end ) 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 6bc2cfda5c..c5a820bdba 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua @@ -192,4 +192,36 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial valve and lock event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.auto.NAME, + capabilities.thermostatMode.thermostatMode.cool.NAME, + capabilities.thermostatMode.thermostatMode.heat.NAME, + capabilities.thermostatMode.thermostatMode.emergency_heat.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatFanMode.supportedThermostatFanModes({ + capabilities.thermostatFanMode.thermostatFanMode.auto.NAME, + capabilities.thermostatFanMode.thermostatFanMode.on.NAME, + capabilities.thermostatFanMode.thermostatFanMode.circulate.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send( { mock_device.id, FanControl.attributes.FanMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.SystemMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.ControlSequenceOfOperation:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(ENDPOINT) }) + end +) + + test.run_registered_tests() 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 e3d30b04c7..628dea32eb 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua @@ -361,5 +361,95 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting the thermostat mode should generate the appropriate messages", + function () + test.socket.capability:__queue_receive({ mock_device.id, { component = "main", capability = capabilities.thermostatMode.ID, command = "setThermostatMode", args = {"eco"} } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, Thermostat.ID, THERMOSTAT_SETPOINT_CMD_ID, MFG_CODE, string.char(0x00, (math.floor(21.0 * 100) & 0xFF), (math.floor(21.0 * 100) >> 8))) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + 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.id, "init" }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ThermostatUIConfig.attributes.KeypadLockout:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + } + ) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, + MFG_CODE + )}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + EXTERNAL_WINDOW_OPEN_DETECTION, + MFG_CODE + )}) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared())) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { EXTERNAL_WINDOW_OPEN_DETECTION, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) test.run_registered_tests() 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 3eeaa383f7..0d8365a521 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 @@ -713,5 +713,19 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end) +test.register_coroutine_test("ThermostatRunningState reporting shoulb create the appropriate events", function() + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0001)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="heating"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0002)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="cooling"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0004)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="fan only"}))) +end) test.run_registered_tests() 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 dd5e425549..d0951b2958 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 @@ -481,4 +481,52 @@ test.register_message_test( } ) +test.register_coroutine_test("Setting the heating setpoint should generate the appropriate messages", function() + test.socket.capability:__queue_receive({mock_device.id, { + component = "main", + capability = capabilities.thermostatHeatingSetpoint.ID, + command = "setHeatingSetpoint", + args = {21} + }}) + test.socket.zigbee:__expect_send({mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2100)}) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) +end) + +test.register_coroutine_test( + "Setting thermostat mode to eco should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"eco"}} + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:write(mock_device, ThermostatSystemMode.HEAT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE, data_types.Enum8, 0x05) + }) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE) + }) + end +) + test.run_registered_tests() 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 312a8a5890..ab7f4c19cc 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua @@ -474,4 +474,39 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_device) + }) + end +) + test.run_registered_tests() From 0c9bb1df7f7417e44806732c617e23a23eae95a8 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 17 Oct 2025 14:56:37 -0700 Subject: [PATCH 205/449] Merge pull request #2475 from SmartThingsCommunity/revert/WWSTCERT-7351 Revert WWSTCERT-7351 --- drivers/SmartThings/matter-switch/fingerprints.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 508e481144..b95258f7fe 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -438,13 +438,6 @@ matterManufacturer: productId: 0x0016 deviceProfileName: light-color-level-2000K-7000K -# Govee - - id: "4999/24584" - deviceLabel: Govee Smart Bulb - vendorId: 0x1387 - productId: 0x6008 - deviceProfileName: light-color-level-2700K-6500K - # Hue - id: "4107/2049" deviceLabel: Hue W 1600 A21 E26 1P NAM From fc8f3237183e03c5df109a3141cc614f0ea72de4 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 17 Oct 2025 13:10:33 -0700 Subject: [PATCH 206/449] Merge pull request #2474 from SmartThingsCommunity/new_device/WWSTCERT-8293 WWSTCERT-8293 Meross Smart Wi-Fi Thermostat --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 4865bb00db..8358e3164e 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -44,6 +44,12 @@ matterManufacturer: vendorId: 0x1206 productId: 0x0001 deviceProfileName: thermostat-nostate-nobattery + #Meross + - id: "4933/57345" + deviceLabel: Smart Wi-Fi Thermostat + vendorId: 0x1345 + productId: 0xE001 + deviceProfileName: thermostat-fan-nostate-nobattery #Siterwell - id: "4736/769" deviceLabel: Siterwell Radiator Thermostat From c16770debb287f46f9645c121e118441fd118be7 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 20 Oct 2025 15:36:59 -0700 Subject: [PATCH 207/449] Merge pull request #2477 from SmartThingsCommunity/new_device/WWSTCERT-8285 WWSTCERT-8285 NodOn Zigbee Temperature and Humidity Sensor --- .../SmartThings/zigbee-humidity-sensor/fingerprints.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index 9ab5af08d6..5096f9cb25 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -88,13 +88,18 @@ zigbeeManufacturer: manufacturer: Third Reality, Inc model: 3RSM0147Z deviceProfileName: humidity-temp-battery + - id: "NodOn/STPH-4-1-00" + deviceLabel: Zigbee Temperature and Humidity Sensor + manufacturer: NodOn + model: STPH-4-1-00 + deviceProfileName: humidity-temp-battery zigbeeGeneric: - id: "HumidityTempGeneric" deviceLabel: Multipurpose Sensor deviceIdentifiers: - 0x0302 - clusters: - server: + clusters: + server: - 0x0402 # Temperature Measurement Cluster - 0x0405 # Relative Humidity Measurement Cluster deviceProfileName: humidity-temperature From a182c5c0bd72813298c3704ba01c56907aca430e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 22 Oct 2025 14:21:28 -0700 Subject: [PATCH 208/449] WWSTCERT-8297 Meross Smart Wi-Fi Switch (#2473) * WWSTCERT-8297 Meross Smart Wi-Fi Switch WWSTCERT-8300 Meross Smart Wi-Fi Switch WWSTCERT-8303 Meross Smart Wi-Fi Power Strip * update profile --- .../SmartThings/matter-switch/fingerprints.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b95258f7fe..c822693390 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -795,6 +795,22 @@ matterManufacturer: vendorId: 0x102E productId: 0x2250 deviceProfileName: button-battery +#Meross + - id: "4933/40987" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA01B + deviceProfileName: switch-binary + - id: "4933/40978" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA012 + deviceProfileName: switch-binary + - id: "4933/45057" + deviceLabel: Smart Wi-Fi Power Strip + vendorId: 0x1345 + productId: 0xB001 + deviceProfileName: switch-binary #Nanoleaf - id: "Nanoleaf NL53" deviceLabel: Essentials BR30 From 6a07220f8e7d417d57357474e379d85c103479ab Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 22 Oct 2025 14:27:43 -0700 Subject: [PATCH 209/449] WWSTCERT-8340 Meross Smart Wi-Fi Roller Shutter Timer --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d7785021c1..63c3aa3254 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -36,6 +36,12 @@ matterManufacturer: vendorId: 0x153B productId: 0x3801 deviceProfileName: window-covering-tilt +# Meross + - id: "4933/61453" + deviceLabel: Smart Wi-Fi Roller Shutter Timer + vendorId: 0x1345 + productId: 0xF00D + deviceProfileName: window-covering # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain From bf2f8be5419cb105e39a46b5b00b34187941c663 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 22 Oct 2025 14:30:15 -0700 Subject: [PATCH 210/449] WWSTCERT-8415 Decora Smart Motion Sensing Dimmer Switch --- 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 c822693390..93339aaa8e 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -636,6 +636,11 @@ matterManufacturer: vendorId: 0x109B productId: 0x1002 deviceProfileName: switch-level + - id: "4251/4102" + deviceLabel: Decora Smart Motion Sensing Dimmer Switch + vendorId: 0x109B + productId: 0x1006 + deviceProfileName: light-level-motion #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From d019056122fa737e70d857070a048c32eb65c5ae Mon Sep 17 00:00:00 2001 From: zhou chengyu <2865135216@qq.com> Date: Thu, 23 Oct 2025 13:26:53 +0800 Subject: [PATCH 211/449] Added three WISTAR Smart Vertical Blind Motors Signed-off-by: zhou chengyu <2865135216@qq.com> --- .../matter-window-covering/fingerprints.yml | 15 +++++++++++++++ tools/localizations/cn.csv | 3 +++ 2 files changed, 18 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d7785021c1..3f648bfa04 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -156,6 +156,21 @@ matterManufacturer: vendorId: 0x1457 productId: 0x0002 deviceProfileName: window-covering-battery + - id: "5207/22" + deviceLabel: WISTAR WSCMXH Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0016 + deviceProfileName: window-covering-tilt + - id: "5207/23" + deviceLabel: WISTAR WSCMXF Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0017 + deviceProfileName: window-covering-tilt + - id: "5207/24" + deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor + vendorId: 0x1457 + productId: 0x0018 + deviceProfileName: window-covering-tilt #Yooksmart - id: "5411/1052" deviceLabel: Smart WindowCovering Series diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index d9105f0949..7e0cc85dbf 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -116,4 +116,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 +"WISTAR WSCMXH Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXH +"WISTAR WSCMXF Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF +"WISTAR WSCMXF-LED Smart Vertical Blind Motor",威仕达智能梦幻帘电机 WSCMXF-LED "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H From 15241d5ac9e76c6f088f8c6d82381fa0d9b17cd2 Mon Sep 17 00:00:00 2001 From: "cameron.penz" Date: Thu, 23 Oct 2025 09:50:16 -0500 Subject: [PATCH 212/449] add sec.appliedHubGroupMemberState capability to v4-hub profile. --- drivers/SmartThings/hub/profiles/v4-hub.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/SmartThings/hub/profiles/v4-hub.yml b/drivers/SmartThings/hub/profiles/v4-hub.yml index f7c85625b6..b1b882c8b6 100644 --- a/drivers/SmartThings/hub/profiles/v4-hub.yml +++ b/drivers/SmartThings/hub/profiles/v4-hub.yml @@ -18,6 +18,8 @@ components: version: 1 - id: sec.wifiConfiguration version: 1 + - id: sec.appliedHubGroupMemberState + version: 1 categories: - name: Hub categoryType: manufacturer From 8d044bcc6ca1cfca4b2df886bc6ae78fc925a97d Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Thu, 23 Oct 2025 10:01:49 -0500 Subject: [PATCH 213/449] fix: guard against iterating over potential nils --- .../SmartThings/sonos/src/api/event_handlers.lua | 2 +- .../sonos/src/api/sonos_connection.lua | 7 ++++--- .../sonos/src/api/sonos_ssdp_discovery.lua | 8 ++++---- .../sonos/src/api/sonos_websocket_router.lua | 16 ++++++++-------- drivers/SmartThings/sonos/src/cosock/bus.lua | 4 ++-- drivers/SmartThings/sonos/src/sonos_driver.lua | 12 ++++++------ drivers/SmartThings/sonos/src/sonos_state.lua | 11 ++++++----- drivers/SmartThings/sonos/src/ssdp.lua | 6 +++--- drivers/SmartThings/sonos/src/utils.lua | 4 ++-- 9 files changed, 36 insertions(+), 34 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/event_handlers.lua b/drivers/SmartThings/sonos/src/api/event_handlers.lua index ad9b697740..eeb461f7c9 100644 --- a/drivers/SmartThings/sonos/src/api/event_handlers.lua +++ b/drivers/SmartThings/sonos/src/api/event_handlers.lua @@ -65,7 +65,7 @@ function CapEventHandlers.handle_group_update(device, group_info) end function CapEventHandlers.handle_audio_clip_status(device, clips) - for _, clip in ipairs(clips) do + for _, clip in ipairs(clips or {}) do if clip.status == "ACTIVE" then log.debug(st_utils.stringify_table(clip, "Playing Audio Clip: ", false)) elseif clip.status == "DONE" then diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 48c8828fc8..3dd90bb79c 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -55,7 +55,7 @@ local _update_subscriptions_helper = function( command, reply_tx ) - for _, namespace in ipairs(namespaces) do + for _, namespace in ipairs(namespaces or {}) do local wss_msg_header = { namespace = namespace, command = command, @@ -467,9 +467,10 @@ function SonosConnection.new(driver, device) if body.version ~= favorites_version then favorites_version = body.version - local household = self.driver.sonos:get_household(header.householdId) or { groups = {} } + local household = self.driver.sonos:get_household(header.householdId) + or self.driver.sonos.EMPTY_HOUSEHOLD - for group_id, group in pairs(household.groups) do + for group_id, group in pairs(household.groups or {}) do local coordinator_id = self.driver.sonos:get_coordinator_for_group(header.householdId, group_id) local coordinator_player = household.players[coordinator_id] diff --git a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua index 5baf30efe8..ddc1d0842d 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_ssdp_discovery.lua @@ -115,7 +115,7 @@ function SpeakerDiscoveryInfo.new(ssdp_info, discovery_info) ret.rest_path = rest_path end - for k, v in pairs(SpeakerDiscoveryInfo) do + for k, v in pairs(SpeakerDiscoveryInfo or {}) do rawset(ret, k, v) end @@ -227,7 +227,7 @@ local function make_persistent_task_impl( log.warn(string.format("Select error: %s", select_err)) end - for _, receiver in ipairs(recv_ready) do + for _, receiver in ipairs(recv_ready or {}) do if receiver == interval_timer then interval_timer:handled() ssdp_search_handle:multicast_m_search() @@ -331,7 +331,7 @@ function SonosPersistentSsdpTask:get_all_known() -- make a shallow copy of the table so it doesn't get clobbered -- the player info itself is a read-only proxy table as well local known = {} - for id, info in pairs(self.player_info_by_sonos_ids) do + for id, info in pairs(self.player_info_by_sonos_ids or {}) do known[id] = info end return known @@ -454,7 +454,7 @@ function sonos_ssdp.spawn_persistent_ssdp_task() log.debug( st_utils.stringify_table(waiting_handles, "waiting for unique keys and mac addresses", true) ) - for _, reply_tx in ipairs(waiting_handles) do + for _, reply_tx in ipairs(waiting_handles or {}) do reply_tx:send(speaker_info) end diff --git a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua index 7e15855e89..c19e35274b 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua @@ -39,7 +39,7 @@ local pending_close = {} cosock.spawn(function() while true do - for _, unique_key in ipairs(pending_close) do -- close any sockets pending close before selecting/receiving on them + for _, unique_key in ipairs(pending_close or {}) do -- close any sockets pending close before selecting/receiving on them local wss = websockets[unique_key] if wss ~= nil then log.trace(string.format("Closing websocket for player %s", unique_key)) @@ -60,7 +60,7 @@ cosock.spawn(function() pending_close = {} local socks = { control_rx } - for _, wss in pairs(websockets) do + for _, wss in pairs(websockets or {}) do table.insert(socks, wss) end local receivers, _, err = socket.select(socks, nil, 10) @@ -71,7 +71,7 @@ cosock.spawn(function() log.error("Error in Websocket Router event loop: " .. err) end else - for _, recv in ipairs(receivers) do + for _, recv in ipairs(receivers or {}) do if recv.link and recv.link.queue and #recv.link.queue == 0 then -- workaround a bug in receiving log.warn("attempting to receive on empty channel") goto continue @@ -93,7 +93,7 @@ cosock.spawn(function() elseif err == "closed" and recv.id then -- closed websocket log.trace(string.format("Websocket %s closed", tostring(recv.id))) local still_open_sockets = {} - for unique_key, wss in pairs(websockets) do + for unique_key, wss in pairs(websockets or {}) do if wss.id ~= recv.id then still_open_sockets[unique_key] = wss end @@ -207,7 +207,7 @@ local function _make_websocket(url_table, api_key) local headers = SonosApi.make_headers(api_key) local config = LustreConfig.default():protocol("v1.api.smartspeaker.audio") - for k, v in pairs(headers) do + for k, v in pairs(headers or {}) do config = config:header(k, v) end @@ -313,7 +313,7 @@ end function SonosWebSocketRouter.cleanup_unused_sockets(driver) log.trace("Begin cleanup of unused websockets") local should_keep = {} - for unique_key, _ in pairs(websockets) do + for unique_key, _ in pairs(websockets or {}) do local household_id, player_id = unique_key:match("(.*)/(.*)") local is_joined = driver.sonos:get_device_id_for_player(household_id, player_id) ~= nil log.debug(string.format("Is Player %s joined? %s", player_id, is_joined)) @@ -322,7 +322,7 @@ function SonosWebSocketRouter.cleanup_unused_sockets(driver) local known_devices = driver:get_devices() - for _, device in ipairs(known_devices) do + for _, device in ipairs(known_devices or {}) do local household_id, coordinator_id = driver.sonos:get_coordinator_for_device(device) local coordinator_unique_key, bad_key_part = utils.sonos_unique_key(household_id, coordinator_id) @@ -340,7 +340,7 @@ function SonosWebSocketRouter.cleanup_unused_sockets(driver) end end - for unique_key, keep in pairs(should_keep) do + for unique_key, keep in pairs(should_keep or {}) do if not keep then SonosWebSocketRouter.close_socket_for_player(unique_key) end diff --git a/drivers/SmartThings/sonos/src/cosock/bus.lua b/drivers/SmartThings/sonos/src/cosock/bus.lua index adb36c4880..f0a248753c 100644 --- a/drivers/SmartThings/sonos/src/cosock/bus.lua +++ b/drivers/SmartThings/sonos/src/cosock/bus.lua @@ -159,7 +159,7 @@ end function __sender_mt:close() self._bus_inner.closed = true local existing_links = self._bus_inner.receiver_links - for _, link in pairs(existing_links) do + for _, link in pairs(existing_links or {}) do if link.waker then link.waker() end @@ -176,7 +176,7 @@ end function __sender_mt:send(msg) if not self._bus_inner.closed then -- wapping in table allows `nil` to be sent as a message - for _, link in pairs(self._bus_inner.receiver_links) do + for _, link in pairs(self._bus_inner.receiver_links or {}) do table.insert(link.queue, { msg = msg }) if link.waker then link.waker() diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 3d8b22ecf3..5b9977e50d 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -134,7 +134,7 @@ function SonosDriver:oauth_info_event_subscribe() end function SonosDriver:update_after_startup_state_received() - for k, v in pairs(self.hub_augmented_driver_data) do + for k, v in pairs(self.hub_augmented_driver_data or {}) do local decode_success, decoded = pcall(json.decode, v) if decode_success then self:handle_augmented_data_change(k, decoded) @@ -212,7 +212,7 @@ function SonosDriver:handle_startup_state_received() token_refresher.spawn_token_refresher(self) end self.startup_state_received = true - for _, device in pairs(self.devices_waiting_for_startup_state) do + for _, device in pairs(self.devices_waiting_for_startup_state or {}) do SonosDriverLifecycleHandlers.initialize_device(self, device) end self.devices_waiting_for_startup_state = {} @@ -291,7 +291,7 @@ function SonosDriver:check_auth(info_or_device) end local unauthorized = false - for _, api_key in pairs(SonosApi.api_keys) do + for _, api_key in pairs(SonosApi.api_keys or {}) do local headers = SonosApi.make_headers(api_key, maybe_token and maybe_token.accessToken) local response, response_err = SonosApi.RestApi.get_groups_info(rest_url, household_id, headers) @@ -422,13 +422,13 @@ local function make_ssdp_event_handler( local recv_ready, _, select_err = cosock.socket.select(receivers, nil, nil) if recv_ready then - for _, receiver in ipairs(recv_ready) do + for _, receiver in ipairs(recv_ready or {}) do if oauth_token_subscription ~= nil and receiver == oauth_token_subscription then local token_evt, receive_err = oauth_token_subscription:receive() if not token_evt then log.warn(string.format("Error on token event bus receive: %s", receive_err)) else - for _, event in pairs(unauthorized) do + for _, event in pairs(unauthorized or {}) do -- shouldn't need a nil check on the ssdp_task here since this whole function -- won't get called unless the task is successfully spawned. driver.ssdp_task:publish(event) @@ -696,7 +696,7 @@ function SonosDriver.new_driver_template() }, } - for k, v in pairs(SonosDriver) do + for k, v in pairs(SonosDriver or {}) do template[k] = v end diff --git a/drivers/SmartThings/sonos/src/sonos_state.lua b/drivers/SmartThings/sonos/src/sonos_state.lua index 4fe27ccdc6..faf466c6a3 100644 --- a/drivers/SmartThings/sonos/src/sonos_state.lua +++ b/drivers/SmartThings/sonos/src/sonos_state.lua @@ -85,6 +85,7 @@ local _STATE = { --- @class SonosState local SonosState = {} SonosState.__index = SonosState +SonosState.EMPTY_HOUSEHOLD = _STATE.households:get_or_init("__EMPTY") ---@param device SonosDevice ---@param info SpeakerDiscoveryInfo @@ -361,10 +362,10 @@ function SonosState:update_household_info(id, groups_event, driver) local groups, players = groups_event.groups, groups_event.players - for _, group in ipairs(groups) do + for _, group in ipairs(groups or {}) do household.groups[group.id] = { id = group.id, coordinator_id = group.coordinatorId, player_ids = group.playerIds } - for _, playerId in ipairs(group.playerIds) do + for _, playerId in ipairs(group.playerIds or {}) do household.player_to_group[playerId] = group.id end end @@ -372,15 +373,15 @@ function SonosState:update_household_info(id, groups_event, driver) -- Iterate through the players and track all the devices associated with them -- for bonded set tracking. local log_devices_error = false - for _, player in ipairs(players) do + for _, player in ipairs(players or {}) do -- Prefer devices because deviceIds is deprecated but all we care about is -- the ID so either way is fine. if type(player.devices) == "table" then - for _, device in ipairs(player.devices) do + for _, device in ipairs(player.devices or {}) do update_device_info(driver, player, household, known_bonded_players, device.id) end elseif type(player.deviceIds) == "table" then - for _, device_id in ipairs(player.deviceIds) do + for _, device_id in ipairs(player.deviceIds or {}) do update_device_info(driver, player, household, known_bonded_players, device_id) end else diff --git a/drivers/SmartThings/sonos/src/ssdp.lua b/drivers/SmartThings/sonos/src/ssdp.lua index 7e3b06a198..3e11b92d04 100644 --- a/drivers/SmartThings/sonos/src/ssdp.lua +++ b/drivers/SmartThings/sonos/src/ssdp.lua @@ -64,7 +64,7 @@ end ---@diagnostic disable-next-line: inject-field function Ssdp.check_headers_contain(headers, keys_to_check) local missing = {} - for _, header_key in ipairs(keys_to_check) do + for _, header_key in ipairs(keys_to_check or {}) do if headers:get_one(header_key) == nil then table.insert(missing, header_key) end @@ -197,7 +197,7 @@ end --- the search end time will be pushed out based on the `mx` parameter, extending the amount of --- time that `receive_m_search_response` will return results with a timeout. function _ssdp_mt:multicast_m_search() - for term, _ in pairs(self.search_terms) do + for term, _ in pairs(self.search_terms or {}) do local multicast_msg = table.concat({ "M-SEARCH * HTTP/1.1", "HOST: 239.255.255.250:1900", @@ -284,7 +284,7 @@ function _ssdp_mt:next_msearch_response() ) end - for _, location in ipairs(location_candidates) do + for _, location in ipairs(location_candidates or {}) do local location_host_match = location:match("http://([^,/]+):[^/]+/.+%.xml") if location_host_match ~= nil then possible_locations[location_host_match] = location diff --git a/drivers/SmartThings/sonos/src/utils.lua b/drivers/SmartThings/sonos/src/utils.lua index 1aa299b629..0b646252a2 100644 --- a/drivers/SmartThings/sonos/src/utils.lua +++ b/drivers/SmartThings/sonos/src/utils.lua @@ -319,7 +319,7 @@ function utils.deep_table_eq(tbl1, tbl2) if tbl1 == tbl2 then return true elseif type(tbl1) == "table" and type(tbl2) == "table" then - for key1, value1 in pairs(tbl1) do + for key1, value1 in pairs(tbl1 or {}) do local value2 = tbl2[key1] if value2 == nil then @@ -337,7 +337,7 @@ function utils.deep_table_eq(tbl1, tbl2) end -- check for missing keys in tbl1 - for key2, _ in pairs(tbl2) do + for key2, _ in pairs(tbl2 or {}) do if tbl1[key2] == nil then return false end From e7f9521865cb9329b5f2a8570e94d28a8920bc37 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Thu, 23 Oct 2025 10:04:28 -0500 Subject: [PATCH 214/449] fix: update casing of fallback value from camel to snake --- drivers/SmartThings/sonos/src/api/sonos_connection.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index 3dd90bb79c..b6b89ac082 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -405,7 +405,7 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } + local group = household.groups[header.groupId] or { player_ids = {} } for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device @@ -429,7 +429,7 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } + local group = household.groups[header.groupId] or { player_ids = {} } for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device @@ -452,7 +452,7 @@ function SonosConnection.new(driver, device) ) return end - local group = household.groups[header.groupId] or { playerIds = {} } + local group = household.groups[header.groupId] or { player_ids = {} } for _, player_id in ipairs(group.player_ids) do local device_for_player = self.driver:device_for_player(header.householdId, player_id) --- we've seen situations where these messages can be processed while a device From 8e3bc94ea4549405fb3639c129328664ab016f66 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Thu, 23 Oct 2025 10:13:55 -0500 Subject: [PATCH 215/449] fix: guard against potential nil table method call --- drivers/SmartThings/sonos/src/api/sonos_connection.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/sonos/src/api/sonos_connection.lua b/drivers/SmartThings/sonos/src/api/sonos_connection.lua index b6b89ac082..471c3a04fb 100644 --- a/drivers/SmartThings/sonos/src/api/sonos_connection.lua +++ b/drivers/SmartThings/sonos/src/api/sonos_connection.lua @@ -259,7 +259,9 @@ local function _oauth_reconnect_task(sonos_conn) if unauthorized then sonos_conn.driver:alert_unauthorized() - local token, channel_error = token_receive_handle:receive() + local token, channel_error = + (token_receive_handle and token_receive_handle:receive()) or nil, + "no token receive handle" if not token then log.warn(string.format("Error requesting token: %s", channel_error)) local _, get_token_err = sonos_conn.driver:get_oauth_token() From 0999cebf1315ed08b2c662db40e78e020fecb375 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 23 Oct 2025 14:18:42 -0500 Subject: [PATCH 216/449] add comments to deprecated profiles --- .../matter-switch/profiles/light-color-level-1800K-6500K.yml | 1 + .../matter-switch/profiles/light-color-level-2000K-7000K.yml | 1 + .../matter-switch/profiles/light-color-level-2200K-6500K.yml | 1 + .../matter-switch/profiles/light-color-level-2700K-6500K.yml | 1 + .../light-color-level-illuminance-motion-1000K-15000K.yml | 1 + .../profiles/light-level-ColorTemperature-1500-9000k.yml | 1 + .../profiles/light-level-colorTemperature-2200K-6500K.yml | 1 + .../profiles/light-level-colorTemperature-2700K-6500K.yml | 1 + .../profiles/light-level-colorTemperature-2710k-6500k.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-2.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-3.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-4.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-5.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-6.yml | 1 + drivers/SmartThings/matter-switch/profiles/switch-7.yml | 1 + drivers/SmartThings/matter-thermostat/profiles/fan.yml | 1 + 16 files changed, 16 insertions(+) diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml index 1638962b8f..58c8e0fca6 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-1800K-6500K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml index 3fb0742f0d..4772e22f66 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2000K-7000K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml index f6c2269431..4977423135 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2200K-6500K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml index ebdaa520c1..dbef511bbb 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-2700K-6500K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml index 0b815134b3..dd48e4e81a 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-color-level-illuminance-motion-1000K-15000K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml index e2542bef5c..f19e55ec9c 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-1500k-9000k components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml index 2348027636..79d6556485 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2200K-6500K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml index 1a1e957b0c..75b2bd488c 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2700K-6500K components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml index 46a6444e92..b49179036f 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: light-level-colorTemperature-2710k-6500k components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-2.yml b/drivers/SmartThings/matter-switch/profiles/switch-2.yml index 45fac9402f..f2a1f35e87 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-2.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-2.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-2 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-3.yml b/drivers/SmartThings/matter-switch/profiles/switch-3.yml index 050675b28c..a9a0756287 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-3.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-3.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-3 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-4.yml b/drivers/SmartThings/matter-switch/profiles/switch-4.yml index fd81619b44..c6e526a0a7 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-4.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-4.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-4 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-5.yml b/drivers/SmartThings/matter-switch/profiles/switch-5.yml index 5971405fe5..257da9cdfb 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-5.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-5.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-5 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-6.yml b/drivers/SmartThings/matter-switch/profiles/switch-6.yml index 882738a9d6..3e5136dc52 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-6.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-6.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-6 components: - id: main diff --git a/drivers/SmartThings/matter-switch/profiles/switch-7.yml b/drivers/SmartThings/matter-switch/profiles/switch-7.yml index bbc075ffe3..14536f7ccf 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-7.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-7.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: switch-7 components: - id: main diff --git a/drivers/SmartThings/matter-thermostat/profiles/fan.yml b/drivers/SmartThings/matter-thermostat/profiles/fan.yml index 5c9792309e..e86941d625 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/fan.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/fan.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: fan components: - id: main From 48246bc68db44d1908e5850ce74f3e53ea036f50 Mon Sep 17 00:00:00 2001 From: kdbang Date: Fri, 24 Oct 2025 04:20:18 +0900 Subject: [PATCH 217/449] Merge pull request #2483 from kdbang/enhance_code_coverage_zigbee_window_treatment Enhance code coverage zigbee-window-treatment --- .../src/test/test_aqara_thermostat.lua | 100 ++++++ .../src/test/test_leviton_rc.lua | 32 ++ .../src/test/test_popp_thermostat.lua | 90 +++++ .../src/test/test_resideo_dt300st_m000.lua | 14 + .../test_stelpro_ki_zigbee_thermostat.lua | 48 +++ .../src/test/test_stelpro_thermostat.lua | 35 ++ .../test_zigbee_window_shade_battery_ikea.lua | 35 ++ .../test_zigbee_window_treatment_aqara.lua | 147 +++++++- ...ow_treatment_aqara_roller_shade_rotate.lua | 138 ++++++++ .../test_zigbee_window_treatment_somfy.lua | 22 ++ .../test_zigbee_window_treatment_vimar.lua | 328 ++++++++++++++++++ 11 files changed, 987 insertions(+), 2 deletions(-) create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua 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 4a1dc2057f..84f305d428 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -113,6 +113,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected())) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_ALARM_INFORMATION_ID, data_types.Uint32.ID, 0x00000000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.hardwareFault.hardwareFault.clear())) end ) @@ -128,6 +138,26 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", valveCalibration.calibrationState.calibrationSuccess())) + + local attr_report_data_1 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationPending())) + + local attr_report_data_2 = { + { PRIVATE_VALVE_RESULT_CALIBRATION_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + valveCalibration.calibrationState.calibrationFailure())) end ) @@ -145,6 +175,18 @@ test.register_coroutine_test( capabilities.thermostatMode.thermostatMode.manual())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -162,6 +204,54 @@ test.register_coroutine_test( capabilities.valve.valve.closed())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint","stse.valveCalibration","thermostatMode","lock"}))) + + local attr_report_data_1 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.manual())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_2 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_2, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({""}))) + + local attr_report_data_3 = { + { PRIVATE_THERMOSTAT_OPERATING_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_3, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatMode.thermostatMode.antifreezing())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) + + local attr_report_data_4 = { + { PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_4, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.valve.valve.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end ) @@ -177,6 +267,16 @@ test.register_coroutine_test( }) test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", capabilities.lock.lock.unlocked())) + + local attr_report_data_1 = { + { PRIVATE_CHILD_LOCK_ID, data_types.Uint8.ID, 0x01 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data_1, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("ChildLock", + capabilities.lock.lock.locked())) end ) 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 6bc2cfda5c..c5a820bdba 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua @@ -192,4 +192,36 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + -- The initial valve and lock event should be send during the device's first time onboarding + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.auto.NAME, + capabilities.thermostatMode.thermostatMode.cool.NAME, + capabilities.thermostatMode.thermostatMode.heat.NAME, + capabilities.thermostatMode.thermostatMode.emergency_heat.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.thermostatFanMode.supportedThermostatFanModes({ + capabilities.thermostatFanMode.thermostatFanMode.auto.NAME, + capabilities.thermostatFanMode.thermostatFanMode.on.NAME, + capabilities.thermostatFanMode.thermostatFanMode.circulate.NAME + }, { visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send( { mock_device.id, FanControl.attributes.FanMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.SystemMode:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.ControlSequenceOfOperation:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device):to_endpoint(ENDPOINT) }) + test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(ENDPOINT) }) + end +) + + test.run_registered_tests() 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 e3d30b04c7..628dea32eb 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua @@ -361,5 +361,95 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting the thermostat mode should generate the appropriate messages", + function () + test.socket.capability:__queue_receive({ mock_device.id, { component = "main", capability = capabilities.thermostatMode.ID, command = "setThermostatMode", args = {"eco"} } }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.build_manufacturer_specific_command(mock_device, Thermostat.ID, THERMOSTAT_SETPOINT_CMD_ID, MFG_CODE, string.char(0x00, (math.floor(21.0 * 100) & 0xFF), (math.floor(21.0 * 100) >> 8))) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + 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.id, "init" }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ThermostatUIConfig.attributes.KeypadLockout:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + } + ) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, + MFG_CODE + )}) + test.socket.zigbee:__expect_send({mock_device.id, cluster_base.read_manufacturer_specific_attribute( + mock_device, + Thermostat.ID, + EXTERNAL_WINDOW_OPEN_DETECTION, + MFG_CODE + )}) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { ETRV_WINDOW_OPEN_DETECTION_ATTR_ID, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared())) + end +) + +test.register_coroutine_test( + "Device reported Thermostat WINDOW_OPEN_DETECTION_ATTR_ID attribute", + function() + local attr_report_data = { + { EXTERNAL_WINDOW_OPEN_DETECTION, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Thermostat.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) test.run_registered_tests() 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 3eeaa383f7..0d8365a521 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 @@ -713,5 +713,19 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end) +test.register_coroutine_test("ThermostatRunningState reporting shoulb create the appropriate events", function() + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0001)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="heating"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0002)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="cooling"}))) + test.socket.zigbee:__queue_receive({mock_device.id, + Thermostat.attributes.ThermostatRunningState:build_test_attr_report(mock_device, 0x0004)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.thermostatOperatingState.thermostatOperatingState({value="fan only"}))) +end) test.run_registered_tests() 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 dd5e425549..d0951b2958 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 @@ -481,4 +481,52 @@ test.register_message_test( } ) +test.register_coroutine_test("Setting the heating setpoint should generate the appropriate messages", function() + test.socket.capability:__queue_receive({mock_device.id, { + component = "main", + capability = capabilities.thermostatHeatingSetpoint.ID, + command = "setHeatingSetpoint", + args = {21} + }}) + test.socket.zigbee:__expect_send({mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2100)}) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) +end) + +test.register_coroutine_test( + "Setting thermostat mode to eco should generate correct zigbee messages", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"eco"}} + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:write(mock_device, ThermostatSystemMode.HEAT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE, data_types.Enum8, 0x05) + }) + test.wait_for_events() + + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Thermostat.ID, MFR_SETPOINT_MODE_ATTTRIBUTE, MFG_CODE) + }) + end +) + test.run_registered_tests() 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 312a8a5890..ab7f4c19cc 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua @@ -474,4 +474,39 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.TemperatureDisplayMode:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ThermostatUserInterfaceConfiguration.attributes.KeypadLockout:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_device) + }) + end +) + test.run_registered_tests() 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 4fe49568eb..c0e83e5ab8 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 @@ -67,6 +67,25 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "State transition to unknown", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 255) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.unknown()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + end +) + test.register_coroutine_test( "State transition from opening to partially open", function() @@ -190,4 +209,20 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "SetShadeLevel command handler", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + test.run_registered_tests() 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 aa286d24f7..d669681c0b 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 @@ -39,6 +39,8 @@ local PREF_REVERSE_ON = "\x00\x02\x00\x01\x00\x00\x00" local PREF_SOFT_TOUCH_OFF = "\x00\x08\x00\x00\x00\x01\x00" local PREF_SOFT_TOUCH_ON = "\x00\x08\x00\x00\x00\x00\x00" +local SHADE_STATE_ATTRIBUTE_ID = 0x0404 + local APPLICATION_VERSION = "application_version" local mock_device = test.mock_device.build_test_zigbee_device( @@ -383,8 +385,6 @@ test.register_coroutine_test( end ) --- mock_version_device - test.register_coroutine_test( "Window shade state closed with application version handler", function() @@ -466,4 +466,147 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Set Level handler", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state open", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + mock_device:set_field("initState", "open") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state close", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + mock_device:set_field("initState", "close") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - initial state reverse", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + mock_device:set_field("initState", "reverse") + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + } + ) + test.mock_time.advance_time(2) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, PREF_ATTRIBUTE_ID, + MFG_CODE) + }) + end +) + +test.register_coroutine_test( + "shade state attribute handler - open", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + end +) + +test.register_coroutine_test( + "shade state attribute handler - close", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + end +) + + + test.run_registered_tests() 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 e241a8c81f..a27f85f528 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 @@ -21,6 +21,7 @@ local SinglePrecisionFloat = require "st.zigbee.data_types".SinglePrecisionFloat local t_utils = require "integration_test.utils" local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] local shadeRotateState = capabilities["stse.shadeRotateState"] @@ -39,10 +40,16 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F local PREF_ATTRIBUTE_ID = 0x0401 +local SHADE_STATE_ATTRIBUTE_ID = 0x0404 local PREF_REVERSE_OFF = "\x00\x02\x00\x00\x00\x00\x00" local PREF_REVERSE_ON = "\x00\x02\x00\x01\x00\x00\x00" +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0004 +local ROTATE_DOWN_VALUE = 0x0005 + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("window-treatment-aqara-roller-shade-rotate.yml"), @@ -212,24 +219,52 @@ test.register_coroutine_test( test.register_coroutine_test( "Window shade open cmd handler", function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() 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.GoToLiftPercentage(mock_device, 100) + }) end ) test.register_coroutine_test( "Window shade close cmd handler", function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() 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.GoToLiftPercentage(mock_device, 0) + }) end ) @@ -295,6 +330,8 @@ test.register_coroutine_test( } updates.preferences["stse.reverseRollerShadeDir"] = true test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + test.mock_time.advance_time(1) + test.socket.zigbee:__expect_send( { mock_device.id, @@ -336,4 +373,105 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "SetShadeLevel command handler", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 }} + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + }) + end +) + + +test.register_coroutine_test( + "PREF_ATTRIBUTE_ID attribute handler - notInitialized", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x00\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.notInitialized())) + end +) + +test.register_coroutine_test( + "SHADE_STATE_ATTRIBUTE_ID attribute handler", + function() + local attr_report_data = { + { SHADE_STATE_ATTRIBUTE_ID, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + AnalogOutput.attributes.PresentValue:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "Handle sensitivity adjustment capability", + function() + local attr_report_data = { + { PREF_ATTRIBUTE_ID, data_types.CharString.ID, "\x00\x00\x01\x00\x00\x00\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Basic.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + initializedStateWithGuide.initializedStateWithGuide.initialized())) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.shadeRotateState", component = "main", command = "setRotateState", args = {"rotateUp"} }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }}) )) + + local message = cluster_base.write_manufacturer_specific_attribute(mock_device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + + test.socket.zigbee:__expect_send({ mock_device.id, message }) + + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.shadeRotateState", component = "main", command = "setRotateState", args = {"rotateDown"} }}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }}) )) + + local message = cluster_base.write_manufacturer_specific_attribute(mock_device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + + test.socket.zigbee:__expect_send({ mock_device.id, message }) + end +) + + test.run_registered_tests() 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 c861b3470c..4c66d29257 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 @@ -505,4 +505,26 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "PhysicalClosedLimitLift attribute handler", + function() + --test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.PhysicalClosedLimitLift: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 = 0 } + } + } + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua new file mode 100755 index 0000000000..80a6552d8b --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua @@ -0,0 +1,328 @@ +-- Copyright 2022 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 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 mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-profile-no-firmware-update.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Vimar", + model = "Window_Cov_Module_v1.0", + server_clusters = {0x000, 0x0003, 0x0004, 0x0005, 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( + "State transnsition from opening to partially open", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 99) + } + ) + test.socket.capability:__expect_send( + { + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 1 } + } + } + ) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "State transnsition from opening to closing", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + 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 = 10 } + } + } + ) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 95) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { + capability_id = "windowShadeLevel", component_id = "main", + attribute_id = "shadeLevel", state = { value = 5 } + } + }) + 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 +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + } + }, + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + }, + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window Shade level command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadeLevel", component = "main", + command = "setShadeLevel", args = { 33 } + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(33)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 67) + } + }, + } +) + +test.register_message_test( + "Handle Window Shade Preset command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShadePreset", component = "main", + command = "presetPosition", args = {} + } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + } + } + } +) + +test.register_coroutine_test( + "Refresh necessary attributes", + 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.wait_for_events() + + 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, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes", + 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.wait_for_events() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(mock_device, + 0, + 600, + 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, + clusters.WindowCovering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() From da41283d27274a70a6759be6dcfac37419906fd6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:49:54 -0500 Subject: [PATCH 218/449] Matter Switch: remove 3 unused profiles (#2479) * Matter Switch: remove 3 unused profiles --- .../matter-switch/profiles/m5stack.yml | 22 ----------------- .../matter-switch/profiles/plug-button.yml | 18 -------------- .../matter-switch/profiles/switch-2-level.yml | 24 ------------------- 3 files changed, 64 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/profiles/m5stack.yml delete mode 100644 drivers/SmartThings/matter-switch/profiles/plug-button.yml delete mode 100644 drivers/SmartThings/matter-switch/profiles/switch-2-level.yml diff --git a/drivers/SmartThings/matter-switch/profiles/m5stack.yml b/drivers/SmartThings/matter-switch/profiles/m5stack.yml deleted file mode 100644 index edf662b462..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/m5stack.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: m5stack -components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch -- id: switch2 - capabilities: - - id: switch - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch diff --git a/drivers/SmartThings/matter-switch/profiles/plug-button.yml b/drivers/SmartThings/matter-switch/profiles/plug-button.yml deleted file mode 100644 index 0f9e8b1b56..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/plug-button.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: plug-button -components: - - id: main - capabilities: - - id: switch - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: SmartPlug - - id: button - capabilities: - - id: button - version: 1 - categories: - - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml deleted file mode 100644 index 549a766f93..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/switch-2-level.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: switch-2-level -components: -- id: main - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch -- id: switch2 - capabilities: - - id: switch - version: 1 - - id: switchLevel - version: 1 - - id: refresh - version: 1 - categories: - - name: Switch From cc30d984e28f35cf972901e9981f002d72265a30 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:39:54 -0500 Subject: [PATCH 219/449] Matter Switch: Fix Fingerprint (#2496) --- drivers/SmartThings/matter-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 93339aaa8e..4654f4a84f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -552,7 +552,7 @@ matterManufacturer: deviceLabel: IM Pushbutton Module vendorId: 0x1372 productId: 0x0002 - deviceProfileName: switch-4 + deviceProfileName: 4-button #JUNG - id: "5161/1" deviceLabel: Matter Push button 2-gang From 99ac8b2fe45aec4ea74fbf712b513d86c025774e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 22 Oct 2025 14:21:28 -0700 Subject: [PATCH 220/449] WWSTCERT-8297 Meross Smart Wi-Fi Switch (#2473) * WWSTCERT-8297 Meross Smart Wi-Fi Switch WWSTCERT-8300 Meross Smart Wi-Fi Switch WWSTCERT-8303 Meross Smart Wi-Fi Power Strip * update profile --- .../SmartThings/matter-switch/fingerprints.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b95258f7fe..c822693390 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -795,6 +795,22 @@ matterManufacturer: vendorId: 0x102E productId: 0x2250 deviceProfileName: button-battery +#Meross + - id: "4933/40987" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA01B + deviceProfileName: switch-binary + - id: "4933/40978" + deviceLabel: Smart Wi-Fi Switch + vendorId: 0x1345 + productId: 0xA012 + deviceProfileName: switch-binary + - id: "4933/45057" + deviceLabel: Smart Wi-Fi Power Strip + vendorId: 0x1345 + productId: 0xB001 + deviceProfileName: switch-binary #Nanoleaf - id: "Nanoleaf NL53" deviceLabel: Essentials BR30 From 83e3b75f1f93a5317a52b21116976d06df5c2e6d Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 23 Oct 2025 11:38:41 -0700 Subject: [PATCH 221/449] WWSTCERT-8340 Meross Smart Wi-Fi Roller Shutter Timer (#2488) --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index d7785021c1..63c3aa3254 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -36,6 +36,12 @@ matterManufacturer: vendorId: 0x153B productId: 0x3801 deviceProfileName: window-covering-tilt +# Meross + - id: "4933/61453" + deviceLabel: Smart Wi-Fi Roller Shutter Timer + vendorId: 0x1345 + productId: 0xF00D + deviceProfileName: window-covering # Mamaba - id: "4965/4097" deviceLabel: Wi-Fi Curtain From 6ec3412ba3e92ec6a7cfc7256b37f004b3d1ecca Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 23 Oct 2025 11:39:43 -0700 Subject: [PATCH 222/449] WWSTCERT-8415 Decora Smart Motion Sensing Dimmer Switch (#2489) --- 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 c822693390..93339aaa8e 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -636,6 +636,11 @@ matterManufacturer: vendorId: 0x109B productId: 0x1002 deviceProfileName: switch-level + - id: "4251/4102" + deviceLabel: Decora Smart Motion Sensing Dimmer Switch + vendorId: 0x109B + productId: 0x1006 + deviceProfileName: light-level-motion #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From af874198fd9239b1c0da99204791a9b67991e8cb Mon Sep 17 00:00:00 2001 From: Cornelius117 <72489457+Cornelius117@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:04:35 +0800 Subject: [PATCH 223/449] Add processing logic of motion sensor in button devices (#2459) * Add processing logic of motion sensor in button devices * delete white space * Delete duplicated main component * Add restrictions --- .../profiles/3-button-motion.yml | 26 + .../profiles/6-button-motion.yml | 44 + .../test/test_matter_multi_button_motion.lua | 765 ++++++++++++++++++ .../src/utils/device_configuration.lua | 4 + 4 files changed, 839 insertions(+) create mode 100644 drivers/SmartThings/matter-switch/profiles/3-button-motion.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/6-button-motion.yml create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua diff --git a/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml b/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml new file mode 100644 index 0000000000..2852dd2ccd --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/3-button-motion.yml @@ -0,0 +1,26 @@ +name: 3-button-motion +components: + - id: main + capabilities: + - id: button + version: 1 + - id: motionSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml b/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml new file mode 100644 index 0000000000..f191e95fdc --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/6-button-motion.yml @@ -0,0 +1,44 @@ +name: 6-button-motion +components: + - id: main + capabilities: + - id: button + version: 1 + - id: motionSensor + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController \ No newline at end of file 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 new file mode 100644 index 0000000000..0869671622 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -0,0 +1,765 @@ +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" +local dkjson = require "dkjson" + +local clusters = require "st.matter.generated.zap_clusters" +local button_attr = capabilities.button.button + +local mock_device = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("6-button-motion.yml"), -- on a real device we would switch to this, rather than fingerprint to it + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + endpoints = { + { + endpoint_id = 0, + clusters = {}, + device_types = {} + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 30, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 40, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 50, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 60, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 70, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- OccupancySensor + } + }, + }, +} +) + +-- add device for each mock device +local CLUSTER_SUBSCRIBE_LIST ={ + clusters.OccupancySensing.attributes.Occupancy, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local function expect_configure_buttons() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) + + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.pushed({state_change = false}))) +end + +-- All messages queued and expectations set are done before the driver is actually run +local function test_init() + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here + -- so we can set the proper expectations on those events. + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) -- make sure the cache is populated + + -- added sets a bunch of fields on the device, and calls init + 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.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- init results in subscription interaction + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + --doConfigure sets the provisioning state to provisioned + mock_device:expect_metadata_update({ profile = "6-button-motion" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + -- simulate the profile change update taking affect and the device info changing + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "6-buttons-motion" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + expect_configure_buttons() +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Handle single press sequence, no hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_message_test( + "Handle single press sequence for short release-supported button", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1? + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1? + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press + } +} +) + +test.register_coroutine_test( + "Handle single press sequence for emulated hold on short-release-only button", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 30, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 30, {previous_position = 0} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a long hold on multi button", -- pushes should only be generated from multiPressComplete events + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 50, {new_position = 1} + ) + }) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 50, {previous_position = 0} + ) + }) + end +) + +test.register_coroutine_test( + "Handle single press sequence for a multi press on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_device, 60, {new_position = 1, current_number_of_presses_counted = 2} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.double({state_change = true}))) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on long-release-capable button", -- only a long press event should generate a held event + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 40, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 40, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 40, {previous_position = 0} + ) + }) + end +) + +test.register_coroutine_test( + "Occupancy reports should generate correct messages", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 70, 1) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 70, 0) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + +test.register_coroutine_test( + "Handle long press sequence for a long hold on multi button", + function () + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.held({state_change = true}))) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 60, {previous_position = 0} + ) + }) + end +) + +test.register_message_test( + "Handle single press sequence, with hold", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + } +} +) + +test.register_message_test( + "Handle release after short press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + } +) + +test.register_message_test( + "Handle release after long press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 2 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 10, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of 3 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 60, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Receiving a max press attribute of greater than 6 should only emit up to pushed_6x", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 10, 7 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "pushed_3x", "pushed_4x", "pushed_5x", "pushed_6x"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Handle double press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + +} +) + +test.register_message_test( + "Handle multi press for 4 times", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + }, + +} +) + +test.register_message_test( + "Receiving a max press attribute of 2 should emit correct event", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 50, 2 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) + }, + } +) + +test.register_message_test( + "Handle a long press including MultiPressComplete", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + } + -- no double event +} +) + +test.register_message_test( + "Handle long press followed by single press", { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button6", button_attr.pushed({state_change = true})) + } + } +) +-- run the tests +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index feb21ac193..3870b82f9f 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -135,6 +135,10 @@ function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, if switch_utils.device_type_supports_button_switch_combination(device, main_endpoint) then profile_name = "light-level-" .. profile_name end + local motion_eps = device:get_endpoints(clusters.OccupancySensing.ID) + if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled + profile_name = profile_name .. "-motion" + end local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler device:send(clusters.PowerSource.attributes.AttributeList:read(device)) From 9331e5d7ee9c93c302e0d4fa32b171f5f6c8a86e Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 28 Oct 2025 15:15:30 -0500 Subject: [PATCH 224/449] Matter Switch: Update Vendor Overrides (#2480) * update vendor overrides --- .../generic_handlers/attribute_handlers.lua | 3 +- .../src/test/test_matter_bridge.lua | 12 +++++ .../src/utils/device_configuration.lua | 17 ++---- .../matter-switch/src/utils/switch_fields.lua | 26 +++++---- .../matter-switch/src/utils/switch_utils.lua | 53 ++++++++++--------- 5 files changed, 56 insertions(+), 55 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua index ad8a74a00f..73844f2fb7 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua @@ -361,8 +361,7 @@ function AttributeHandlers.power_source_attribute_list_handler(driver, device, i profile_name = string.format("%d-", #button_eps) .. profile_name end - if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then + if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then profile_name = profile_name .. "-temperature-humidity" end device:try_update_metadata({ profile = profile_name }) 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 54eb21dc88..0d11bc8b5e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -88,6 +88,18 @@ end test.register_coroutine_test( "Profile should not change for devices with aggregator device type (bridges)", function() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_bridge) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_bridge)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) + test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) + test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) + mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init_mock_bridge } ) diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index 3870b82f9f..b2d066ce61 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -50,20 +50,9 @@ function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) end end - -- Check if device has an overridden child profile that differs from the profile that would match - -- the child's device type for the following two cases: - -- 1. To add Electrical Sensor only to the first EDGE_CHILD (light-power-energy-powerConsumption) - -- for the Aqara Light Switch H2. The profile of the second EDGE_CHILD for this device is - -- determined in the "for" loop above (e.g., light-binary) - -- 2. The selected profile for the child device matches the initial profile defined in - -- child_device_profile_overrides - for id, vendor in pairs(fields.child_device_profile_overrides_per_vendor_id) do - for _, fingerprint in ipairs(vendor) do - if device.manufacturer_info.product_id == fingerprint.product_id and - ((device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and child_ep == 1) or profile == fingerprint.initial_profile) then - return fingerprint.target_profile - end - end + -- vendor override checks + if child_ep == switch_utils.get_product_override_field(device, "ep_id") or profile == switch_utils.get_product_override_field(device, "initial_profile") then + profile = switch_utils.get_product_override_field(device, "target_profile") or profile end -- default to "switch-binary" if no profile is found diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 2244eab661..07ad0fdbeb 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -106,19 +106,20 @@ SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_ SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY -SwitchFields.child_device_profile_overrides_per_vendor_id = { +SwitchFields.vendor_overrides = { [0x1321] = { - { product_id = 0x000C, target_profile = "switch-binary", initial_profile = "plug-binary" }, - { product_id = 0x000D, target_profile = "switch-binary", initial_profile = "plug-binary" }, + [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, - [0x115F] = { - { product_id = 0x1003, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1004, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x1005, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - { product_id = 0x1006, target_profile = "light-level-power-energy-powerConsumption" }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - { product_id = 0x1008, target_profile = "light-power-energy-powerConsumption" }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - { product_id = 0x1009, target_profile = "light-power-energy-powerConsumption" }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - { product_id = 0x100A, target_profile = "light-level-power-energy-powerConsumption" }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x115F] = { -- AQARA_MANUFACTURER_ID + [0x1003] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + [0x1004] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) + [0x1005] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) + [0x1008] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) + [0x1009] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) + [0x1006] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x100A] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile } } @@ -150,9 +151,6 @@ SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" SwitchFields.TEMP_MIN = "__temp_min" SwitchFields.TEMP_MAX = "__temp_max" -SwitchFields.AQARA_MANUFACTURER_ID = 0x115F -SwitchFields.AQARA_CLIMATE_SENSOR_W100_ID = 0x2004 - SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second -- When sent with a command, these options mask and override bitmaps cause the command -- to take effect when the switch/light is off. diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 86368e9208..2558429654 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -63,6 +63,14 @@ function utils.mired_to_kelvin(value, minOrMax) end end +function utils.get_product_override_field(device, override_key) + if fields.vendor_overrides[device.manufacturer_info.vendor_id] + and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id] + then + return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key] + end +end + function utils.check_field_name_updates(device) for _, field in ipairs(fields.updated_fields) do if device:get_field(field.current_field_name) then @@ -78,29 +86,18 @@ end --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. function utils.device_type_supports_button_switch_combination(device, endpoint_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == endpoint_id then - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID then - for _, fingerprint in ipairs(fields.child_device_profile_overrides_per_vendor_id[0x115F]) do - if device.manufacturer_info.product_id == fingerprint.product_id then - return false -- For Aqara Dimmer Switch with Button. - end - end - return true - end - end - end + if utils.get_product_override_field(device, "ignore_combo_switch_button") then + return false end - return false + local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID) + return utils.tbl_contains(dimmable_eps, endpoint_id) end --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) - if device.manufacturer_info.vendor_id == fields.AQARA_MANUFACTURER_ID and - device.manufacturer_info.product_id == fields.AQARA_CLIMATE_SENSOR_W100_ID then - -- In case of Aqara Climate Sensor W100, in order to sequentially set the button name to button 1, 2, 3 + -- Buttons should not be set on the main component for the Aqara Climate Sensor W100, + if utils.get_product_override_field(device, "is_climate_sensor_w100") then return device.MATTER_DEFAULT_ENDPOINT end @@ -171,6 +168,19 @@ function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) end +-- get a list of endpoints for a specified device type. +function utils.get_endpoints_by_device_type(device, device_type_id) + local dt_eps = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type_id then + table.insert(dt_eps, ep.endpoint_id) + end + end + end + return dt_eps +end + --helper function to create list of multi press values function utils.create_multi_press_values_list(size, supportsHeld) local list = {"pushed", "double"} @@ -183,14 +193,7 @@ function utils.create_multi_press_values_list(size, supportsHeld) end function utils.detect_bridge(device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.AGGREGATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false + return #utils.get_endpoints_by_device_type(device, fields.AGGREGATOR_DEVICE_TYPE_ID) > 0 end function utils.detect_matter_thing(device) From 604a7be3e5dd5f3d42681e048f2cec07605a8637 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 29 Oct 2025 10:59:56 -0700 Subject: [PATCH 225/449] WWSTCERT-8637 NodOn Zigbee Multifunction Relay Switch --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index e449e1f194..fc6ae4e5c7 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2369,6 +2369,12 @@ zigbeeManufacturer: manufacturer: LAISIAO model: yuba deviceProfileName: switch-smart-bath-heater-laisiao + # NodOn + - id: "NodOn/SIN-4-1-20" + deviceLabel: Zigbee Multifunction Relay Switch + manufacturer: NodOn + model: SIN-4-1-20 + deviceProfileName: basic-switch zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From 8b4b5c88ea4012b6204e559b9a696fb42aadc6c8 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 29 Oct 2025 11:10:01 -0700 Subject: [PATCH 226/449] WWSTCERT-8637 NodOn Zigbee Roller Shutter Relay Switch --- drivers/SmartThings/zigbee-window-treatment/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index 808add60ff..c677cbce4b 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -128,6 +128,11 @@ zigbeeManufacturer: manufacturer: VIVIDSTORM model: VWSDSTUST120H deviceProfileName: projector-screen-VWSDSTUST120H + - id: "NodOn/SIN-4-RS-20" + deviceLabel: Zigbee Roller Shutter Relay Switch + manufacturer: NodOn + model: SIN-4-RS-20 + deviceProfileName: window-treatment-profile zigbeeGeneric: - id: "genericShade" From b53f7bbb468690b0d173b1ad7f2eb2fe5dfd1c14 Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Fri, 31 Oct 2025 04:20:30 +0900 Subject: [PATCH 227/449] WWSTCERT-8471/8468 [Aqara] Wireless Remote Switch T1(Single/Double) (#2458) * add Aqara Wireless Remote Switch T1(Single/Double) * change the categories of the main component from 'Button' to 'RemoteController' in the 'aqara-double-buttons' profile * Added the firmwareUpdate capability to aqara-single-button.yml and renamed the file to one-button-batteryLevel.yml. * Changing the device profile of Aqara Button Devices * dd the firmwareUpdate capability to the aqara-double-buttons device profile. --- .../zigbee-button/fingerprints.yml | 14 +- .../profiles/aqara-double-buttons.yml | 32 ++++ .../profiles/one-button-batteryLevel.yml | 14 ++ .../zigbee-button/src/aqara/init.lua | 160 +++++++++++------- .../src/test/test_aqara_button.lua | 156 ++++++++++------- 5 files changed, 257 insertions(+), 119 deletions(-) create mode 100644 drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml create mode 100644 drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 7acc77387f..61285448f7 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -8,12 +8,22 @@ zigbeeManufacturer: deviceLabel: Aqara Wireless Mini Switch T1 manufacturer: LUMI model: lumi.remote.b1acn02 - deviceProfileName: one-button-battery + deviceProfileName: one-button-batteryLevel - id: "LUMI/lumi.remote.acn003" deviceLabel: Aqara Wireless Remote Switch E1 (Single Rocker) manufacturer: LUMI model: lumi.remote.acn003 - deviceProfileName: one-button-battery + deviceProfileName: one-button-batteryLevel + - id: "LUMI/lumi.remote.b186acn03" + deviceLabel: Aqara Wireless Remote Switch T1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b186acn03 + deviceProfileName: one-button-batteryLevel + - id: "LUMI/lumi.remote.b286acn03" + deviceLabel: Aqara Wireless Remote Switch T1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b286acn03 + deviceProfileName: aqara-double-buttons - id: "HEIMAN/SOS-EM" deviceLabel: HEIMAN Button manufacturer: HEIMAN diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml new file mode 100644 index 0000000000..07a4882783 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons.yml @@ -0,0 +1,32 @@ +name: aqara-double-buttons +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml b/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml new file mode 100644 index 0000000000..005e41256f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/one-button-batteryLevel.yml @@ -0,0 +1,14 @@ +name: one-button-batteryLevel +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 174d775324..b405b506cd 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -29,74 +29,113 @@ local MFG_CODE = 0x115F local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 local PRESENT_ATTRIBUTE_ID = 0x0055 +local COMP_LIST = { "button1", "button2", "all" } local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.remote.b1acn02" }, - { mfr = "LUMI", model = "lumi.remote.acn003" } + ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 } } local configuration = { - { - cluster = MULTISTATE_INPUT_CLUSTER_ID, - attribute = PRESENT_ATTRIBUTE_ID, - minimum_interval = 3, - maximum_interval = 7200, - data_type = data_types.Uint16, - reportable_change = 1 - }, - { - cluster = PowerConfiguration.ID, - attribute = PowerConfiguration.attributes.BatteryVoltage.ID, - minimum_interval = 30, - maximum_interval = 3600, - data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, - reportable_change = 1 - } + { + cluster = MULTISTATE_INPUT_CLUSTER_ID, + attribute = PRESENT_ATTRIBUTE_ID, + minimum_interval = 3, + maximum_interval = 7200, + data_type = data_types.Uint16, + reportable_change = 1 + }, + { + cluster = PowerConfiguration.ID, + attribute = PowerConfiguration.attributes.BatteryVoltage.ID, + minimum_interval = 30, + maximum_interval = 3600, + data_type = PowerConfiguration.attributes.BatteryVoltage.base_type, + reportable_change = 1 + } } local function present_value_attr_handler(driver, device, value, zb_rx) - if value.value == 1 then - device:emit_event(capabilities.button.button.pushed({state_change = true})) - elseif value.value == 2 then - device:emit_event(capabilities.button.button.double({state_change = true})) - elseif value.value == 0 then - device:emit_event(capabilities.button.button.held({state_change = true})) - end + local end_point = zb_rx.address_header.src_endpoint.value + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local evt = capabilities.button.button.held({ state_change = true }) + if value.value == 1 then + evt = capabilities.button.button.pushed({ state_change = true }) + elseif value.value == 2 then + evt = capabilities.button.button.double({ state_change = true }) + end + device:emit_event(evt) + if btn_evt_cnt > 1 then + device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) + end +end +local function battery_level_handler(driver, device, value, zb_rx) + local voltage = value.value + local batteryLevel = "normal" + if voltage <= 25 then + batteryLevel = "critical" + elseif voltage < 28 then + batteryLevel = "warning" + end + device:emit_event(capabilities.batteryLevel.battery(batteryLevel)) end local is_aqara_products = function(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 + local isAqaraProducts = false + if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then + isAqaraProducts = true + end + return isAqaraProducts end local function device_init(driver, device) - battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) - if configuration ~= nil then - for _, attribute in ipairs(configuration) do - device:add_configured_attribute(attribute) - end + battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) end + end end local function added_handler(self, device) - device:emit_event(capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }})) - device:emit_event(capabilities.button.numberOfButtons({value = 1})) - button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) - device:emit_event(capabilities.battery.battery(100)) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + + device:emit_event(capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + device:emit_event(capabilities.batteryLevel.battery.normal()) + device:emit_event(capabilities.batteryLevel.type("CR2032")) + device:emit_event(capabilities.batteryLevel.quantity(1)) + + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + end + end end local function do_configure(driver, device) + local ATTR_ID = PRIVATE_ATTRIBUTE_ID_T1 + local cmd_value = 1 + device:configure() - if device:get_model() == "lumi.remote.b1acn02" then - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, data_types.Uint8, 1)) - elseif device:get_model() == "lumi.remote.acn003" then - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, data_types.Uint8, 2)) + if device:get_model() == "lumi.remote.acn003" then + ATTR_ID = PRIVATE_ATTRIBUTE_ID_E1 + cmd_value = 2 end + device:send(cluster_base.write_manufacturer_specific_attribute(device, + PRIVATE_CLUSTER_ID, ATTR_ID, MFG_CODE, data_types.Uint8, cmd_value)) -- when the wireless switch T1 accesses the network, the gateway sends -- private attribute 0009 to make the device no longer distinguish -- between the standard gateway and the aqara gateway. @@ -105,20 +144,23 @@ local function do_configure(driver, device) end local aqara_wireless_switch_handler = { - NAME = "Aqara Wireless Switch Handler", - lifecycle_handlers = { - init = device_init, - added = added_handler, - doConfigure = do_configure - }, - zigbee_handlers = { - attr = { - [MULTISTATE_INPUT_CLUSTER_ID] = { - [PRESENT_ATTRIBUTE_ID] = present_value_attr_handler - } + NAME = "Aqara Wireless Switch Handler", + lifecycle_handlers = { + init = device_init, + added = added_handler, + doConfigure = do_configure + }, + zigbee_handlers = { + attr = { + [MULTISTATE_INPUT_CLUSTER_ID] = { + [PRESENT_ATTRIBUTE_ID] = present_value_attr_handler + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_level_handler } - }, - can_handle = is_aqara_products + } + }, + can_handle = is_aqara_products } return aqara_wireless_switch_handler 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 b53a966761..1d7b6d090c 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -29,9 +29,10 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local COMP_LIST = { "button1", "button2", "all" } local mock_device_e1 = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("one-button-battery.yml"), + profile = t_utils.get_profile_definition("one-button-batteryLevel.yml"), zigbee_endpoints = { [1] = { id = 1, @@ -43,14 +44,14 @@ local mock_device_e1 = test.mock_device.build_test_zigbee_device( } ) -local mock_device_t1 = test.mock_device.build_test_zigbee_device( +local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("one-button-battery.yml"), + profile = t_utils.get_profile_definition("aqara-double-buttons.yml"), zigbee_endpoints = { [1] = { id = 1, manufacturer = "LUMI", - model = "lumi.remote.b1acn02", + model = "lumi.remote.b286acn03", server_clusters = { 0x0001, 0x0012 } } } @@ -60,41 +61,36 @@ local mock_device_t1 = 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_e1) - test.mock_device.add_test_device(mock_device_t1)end + test.mock_device.add_test_device(mock_device_t1_double_rocker) +end test.set_test_init_function(test_init) test.register_coroutine_test( - "Handle added lifecycle -- e1", + "Handle added lifecycle - T1 double rocker", function() - -- The initial button pushed event should be send during the device's first time onboarding - test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) - -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. - test.socket.device_lifecycle:__queue_receive({ mock_device_e1.id, "added" }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", capabilities.battery.battery(100))) + test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "added" }) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.battery.normal())) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.type("CR2032"))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.batteryLevel.quantity(1))) + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) end -) -test.register_coroutine_test( - "Handle added lifecycle -- t1", - function() - -- The initial button pushed event should be send during the device's first time onboarding - test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.button.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) - -- Avoid sending the initial button pushed event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. - test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed","held","double"}, {visibility = { displayed = false }}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.button.numberOfButtons({value = 1}))) - test.socket.capability:__expect_send(mock_device_t1:generate_test_message("main", capabilities.battery.battery(100))) end ) @@ -116,10 +112,12 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ mock_device_e1.id, - zigbee_test_utils.build_attr_config(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) + zigbee_test_utils.build_attr_config(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, + 0x1C20, data_types.Uint16, 0x0001) }) test.socket.zigbee:__expect_send({ mock_device_e1.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, + cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, 2) }) mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -129,27 +127,31 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle doConfigure lifecycle -- t1", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_bind_request(mock_device_t1, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1, 30, 3600, 1) + mock_device_t1_double_rocker.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1_double_rocker, 30, 3600, 1) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_bind_request(mock_device_t1, zigbee_test_utils.mock_hub_eui, MULTISTATE_INPUT_CLUSTER_ID) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + MULTISTATE_INPUT_CLUSTER_ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1.id, - zigbee_test_utils.build_attr_config(mock_device_t1, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_attr_config(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, + 0x0003, 0x1C20, data_types.Uint16, 0x0001) }) - test.socket.zigbee:__expect_send({ mock_device_t1.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_t1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, + test.socket.zigbee:__expect_send({ mock_device_t1_double_rocker.id, + cluster_base.write_manufacturer_specific_attribute(mock_device_t1_double_rocker, PRIVATE_CLUSTER_ID, + PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, data_types.Uint8, 1) }) - mock_device_t1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + mock_device_t1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -160,10 +162,13 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", capabilities.button.button.pushed({state_change = true}))) end ) @@ -175,10 +180,13 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + capabilities.button.button.double({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", capabilities.button.button.double({state_change = true}))) end ) @@ -190,16 +198,19 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } } test.socket.zigbee:__queue_receive({ - mock_device_e1.id, - zigbee_test_utils.build_attribute_report(mock_device_e1, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) + mock_device_t1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_e1:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", capabilities.button.button.held({state_change = true}))) + test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", + capabilities.button.button.held({ state_change = true }))) end ) test.register_message_test( - "Battery voltage report should be handled", + "Battery Level - Normal", { { channel = "zigbee", @@ -209,9 +220,38 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device_e1:generate_test_message("main", capabilities.battery.battery(100)) + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("normal")) + } + } +) +test.register_message_test( + "Battery Level - Warning", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device_e1.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device_e1, 27) } + }, + { + channel = "capability", + direction = "send", + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("warning")) + } + } +) +test.register_message_test( + "Battery Level - Critical", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device_e1.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device_e1, 20) } + }, + { + channel = "capability", + direction = "send", + message = mock_device_e1:generate_test_message("main", capabilities.batteryLevel.battery("critical")) } } ) - test.run_registered_tests() From 08ea8eb2faf6e6bf17a92c1d94af276cbd8d89d6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 31 Oct 2025 12:15:47 -0500 Subject: [PATCH 228/449] Add greater dynamic profiling to matter switch (#2481) --- .../SmartThings/matter-switch/src/init.lua | 6 +- .../src/test/test_aqara_light_switch_h2.lua | 4 - .../src/test/test_electrical_sensor.lua | 12 +- .../src/test/test_matter_switch.lua | 107 +++++++++ .../test/test_matter_switch_device_types.lua | 4 + .../test_multi_switch_parent_child_lights.lua | 2 + .../test_multi_switch_parent_child_plugs.lua | 2 + .../src/utils/device_configuration.lua | 227 +++++++----------- .../matter-switch/src/utils/switch_fields.lua | 105 +++++--- .../matter-switch/src/utils/switch_utils.lua | 47 +++- 10 files changed, 326 insertions(+), 190 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bbf9ca24ee..c7ac1f4d4f 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -85,10 +85,10 @@ function SwitchLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end - local main_endpoint = switch_utils.find_default_endpoint(device) + local default_endpoint_id = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices for idx, ep in ipairs(device.endpoints) do - if ep.endpoint_id ~= main_endpoint then + if ep.endpoint_id ~= default_endpoint_id then if device:supports_server_cluster(clusters.OnOff.ID, ep) then local child_profile = switch_cfg.assign_child_profile(device, ep) if idx == 1 and string.find(child_profile, "energy") then @@ -101,7 +101,7 @@ function SwitchLifecycleHandlers.device_init(driver, device) id = math.max(id, dt.device_type_id) end for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do - if id == fields.GENERIC_SWITCH_ID and + if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and attr ~= clusters.PowerSource.attributes.BatPercentRemaining and attr ~= clusters.PowerSource.attributes.BatChargeLevel then device:add_subscribed_event(attr) 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 548ce67890..da8066f828 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 @@ -27,10 +27,6 @@ if version.api < 11 then clusters.PowerTopology = require "embedded_clusters.PowerTopology" end -if version.api < 16 then - clusters.Descriptor = require "embedded_clusters.Descriptor" -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.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index d981af3110..06ec130878 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -56,7 +56,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - { device_type_id = 0x010A, device_type_revision = 1 } -- OnOff Plug + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit } } }, @@ -88,10 +88,20 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor } }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit + } + } }, }) local subscribed_attributes_periodic = { + clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, } 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 84ccc9e478..8b3bbd8ae2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -77,6 +77,52 @@ local mock_device_no_hue_sat = test.mock_device.build_test_matter_device({ } }) +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light + } + } + } +}) + +local mock_device_extended_color = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 1}, -- Dimmable Light + {device_type_id = 0x010C, device_type_revision = 1}, -- Color Temperature Light + {device_type_id = 0x010D, device_type_revision = 1}, -- Extended Color Light + } + } + } +}) + local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -146,6 +192,67 @@ local function test_init_no_hue_sat() set_color_mode(mock_device_no_hue_sat, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) end + +local cluster_subscribe_list_color_temp = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds +} + +local function test_init_color_temp() + test.mock_device.add_test_device(mock_device_color_temp) + local subscribe_request = cluster_subscribe_list_color_temp[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list_color_temp) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end + end + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "init" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) + mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_extended_color() + test.mock_device.add_test_device(mock_device_extended_color) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_extended_color) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_extended_color)) + end + end + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "doConfigure" }) + mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +test.register_message_test( + "Test that Color Temperature Light device does not switch profiles", + {}, + { test_init = test_init_color_temp } +) + +test.register_message_test( + "Test that Extended Color Light device does not switch profiles", + {}, + { test_init = test_init_extended_color } +) + test.register_message_test( "On command should send the appropriate commands", { diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 0046244bcd..056c103eed 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -505,6 +505,7 @@ local function test_init_mounted_on_off_control() test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -526,6 +527,7 @@ local function test_init_mounted_dimmable_load_control() test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -566,6 +568,7 @@ local function test_init_parent_child_different_types() test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) + mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) mock_device_parent_child_different_types:expect_device_create({ @@ -617,6 +620,7 @@ local function test_init_light_level_motion() test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 21c9e1087d..cff4c7b60b 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -189,6 +189,7 @@ local function test_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 = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -260,6 +261,7 @@ local function test_init_parent_child_endpoints_non_sequential() test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 03c898b7f5..323efea478 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -146,6 +146,7 @@ local function test_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 = "plug-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -196,6 +197,7 @@ local function test_init_child_profile_override() test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) + mock_device_child_profile_override:expect_metadata_update({ profile = "switch-binary" }) mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_child_profile_override) do diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua index b2d066ce61..23b5ca6873 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua @@ -31,97 +31,62 @@ local DeviceConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} -function SwitchDeviceConfiguration.assign_child_profile(device, child_ep) - local profile +function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) + local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id == child_ep then - -- Some devices report multiple device types which are a subset of - -- a superset device type (For example, Dimmable Light is a superset of - -- On/Off light). This mostly applies to the four light types, so we will want - -- to match the profile for the superset device type. This can be done by - -- matching to the device type with the highest ID - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - profile = fields.device_type_profile_map[id] - break - end - end + -- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER. + local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT) + or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH) + or ep_info.device_types[1] and ep_info.device_types[1].device_type_id - -- vendor override checks - if child_ep == switch_utils.get_product_override_field(device, "ep_id") or profile == switch_utils.get_product_override_field(device, "initial_profile") then - profile = switch_utils.get_product_override_field(device, "target_profile") or profile - end + local generic_profile = fields.device_type_profile_map[primary_dt_id] - -- default to "switch-binary" if no profile is found - return profile or "switch-binary" -end - -function SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) - local num_switch_server_eps = 0 - local parent_child_device = false - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - table.sort(switch_eps) - for idx, ep in ipairs(switch_eps) do - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - num_switch_server_eps = num_switch_server_eps + 1 - if ep ~= main_endpoint then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, num_switch_server_eps) - local child_profile = SwitchDeviceConfiguration.assign_child_profile(device, ep) - driver:try_create_device( - { - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep), - vendor_provided_label = name - } - ) - parent_child_device = true - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end - end + if is_child_device and ( + server_onoff_ep_id == switch_utils.get_product_override_field(device, "ep_id") or + generic_profile == switch_utils.get_product_override_field(device, "initial_profile") + ) then + generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile end - -- If the device is a parent child device, set the find_child function on init. This is persisted because initialize_buttons_and_switches - -- is only run once, but find_child function should be set on each driver init. - if parent_child_device then - device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) - end - - -- this is needed in initialize_buttons_and_switches - return num_switch_server_eps + -- if no supported device type is found, return switch-binary as a generic "OnOff EP" profile + return generic_profile or "switch-binary" end -function SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) - local cluster_id = 0 - for _, ep in ipairs(device.endpoints) do - -- main_endpoint only supports server cluster by definition of get_endpoints() - if main_endpoint == ep.endpoint_id then - for _, dt in ipairs(ep.device_types) do - -- no device type that is not in the switch subset should be considered. - if (fields.ON_OFF_SWITCH_ID <= dt.device_type_id and dt.device_type_id <= fields.ON_OFF_COLOR_DIMMER_SWITCH_ID) then - cluster_id = math.max(cluster_id, dt.device_type_id) - end +function SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) + if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == default_endpoint_id then -- no children will be created + return + end + + local device_num = 0 + table.sort(server_onoff_ep_ids) + for idx, ep_id in ipairs(server_onoff_ep_ids) do + device_num = device_num + 1 + if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint + local name = string.format("%s %d", device.label, device_num) + local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) + driver:try_create_device({ + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = name + }) + if idx == 1 and string.find(child_profile, "energy") then + -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. + device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true}) end - break end end - if fields.device_type_profile_map[cluster_id] then - device:try_update_metadata({profile = fields.device_type_profile_map[cluster_id]}) - end + -- Persist so that the find_child function is always set on each driver init. + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) end -function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, num_button_eps) +function ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, num_button_eps) local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep - if switch_utils.device_type_supports_button_switch_combination(device, main_endpoint) then + if switch_utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then profile_name = "light-level-" .. profile_name end local motion_eps = device:get_endpoints(clusters.OccupancySensing.ID) @@ -136,13 +101,13 @@ function ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, end end -function ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) +function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) -- create component mapping on the main profile button endpoints table.sort(button_eps) local component_map = {} - component_map["main"] = main_endpoint + component_map["main"] = default_endpoint_id for component_num, ep in ipairs(button_eps) do - if ep ~= main_endpoint then + if ep ~= default_endpoint_id then local button_component = "button" if #button_eps > 1 then button_component = button_component .. component_num @@ -192,75 +157,61 @@ end -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- -function DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) - local profile_found = false - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - ButtonDeviceConfiguration.update_button_profile(device, main_endpoint, #button_eps) - -- All button endpoints found will be added as additional components in the profile containing the main_endpoint. - -- The resulting endpoint to component map is saved in the COMPONENT_TO_ENDPOINT_MAP field - ButtonDeviceConfiguration.update_button_component_map(device, main_endpoint, button_eps) - ButtonDeviceConfiguration.configure_buttons(device) - profile_found = true - end - - -- Without support for bindings, only clusters that are implemented as server are counted. This count is handled - -- while building switch child profiles - local num_switch_server_eps = SwitchDeviceConfiguration.create_child_switch_devices(driver, device, main_endpoint) +function DeviceConfiguration.match_profile(driver, device) + local default_endpoint_id = switch_utils.find_default_endpoint(device) + local updated_profile = nil - -- We do not support the Light Switch device types because they require OnOff to be implemented as 'client', which requires us to support bindings. - -- However, this workaround profiles devices that claim to be Light Switches, but that break spec and implement OnOff as 'server'. - -- Note: since their device type isn't supported, these devices join as a matter-thing. - if num_switch_server_eps > 0 and switch_utils.detect_matter_thing(device) then - SwitchDeviceConfiguration.update_devices_with_onOff_server_clusters(device, main_endpoint) - profile_found = true + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then + updated_profile = "water-valve" + if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, + {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then + updated_profile = updated_profile .. "-level" + end end - return profile_found -end -function DeviceConfiguration.match_profile(driver, device) - local main_endpoint = switch_utils.find_default_endpoint(device) - -- initialize the main device card with buttons if applicable, and create child devices as needed for multi-switch devices. - local profile_found = DeviceConfiguration.initialize_buttons_and_switches(driver, device, main_endpoint) - if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then - device:set_find_child(switch_utils.find_child) - end - if profile_found then - return + local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH + if #server_onoff_ep_ids > 0 then + SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) end - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local level_eps = device:get_endpoints(clusters.LevelControl.ID) - local energy_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - local power_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) - local valve_eps = embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) - local profile_name = nil - local level_support = "" - if #level_eps > 0 then - level_support = "-level" - end - if #energy_eps > 0 and #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power-energy-powerConsumption" - elseif #energy_eps > 0 then - profile_name = "plug" .. level_support .. "-energy-powerConsumption" - elseif #power_eps > 0 then - profile_name = "plug" .. level_support .. "-power" - elseif #valve_eps > 0 then - profile_name = "water-valve" - if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID, - {feature_bitmap = clusters.ValveConfigurationAndControl.types.Feature.LEVEL}) > 0 then - profile_name = profile_name .. "-level" + if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then + updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) + local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end + if generic_profile("plug-binary") or generic_profile("plug-level") then + if switch_utils.check_switch_category_vendor_overrides(device) then + updated_profile = string.gsub(updated_profile, "plug", "switch") + else + local electrical_tags = "" + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end + if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end + end + elseif generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then + updated_profile = "light-color-level-fan" + elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + updated_profile = "light-level-motion" + elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then + -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since + -- these may lose fingerprinted Kelvin ranges when dynamically profiled. + return end - elseif #fan_eps > 0 then - profile_name = "light-color-level-fan" end - if profile_name then - device:try_update_metadata({ profile = profile_name }) + + -- initialize the main device card with buttons if applicable + local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then + ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #button_eps) + -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) + ButtonDeviceConfiguration.configure_buttons(device) + return end + + device:try_update_metadata({ profile = updated_profile }) end return { DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, ButtonCfg = ButtonDeviceConfiguration -} +} \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua index 07ad0fdbeb..dad46d4c92 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua @@ -45,35 +45,39 @@ SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 - --- DEVICE TYPES -SwitchFields.AGGREGATOR_DEVICE_TYPE_ID = 0x000E -SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID = 0x0100 -SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID = 0x0101 -SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID = 0x010C -SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID = 0x010D -SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID = 0x010A -SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID = 0x010B -SwitchFields.ON_OFF_SWITCH_ID = 0x0103 -SwitchFields.ON_OFF_DIMMER_SWITCH_ID = 0x0104 -SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID = 0x0105 -SwitchFields.MOUNTED_ON_OFF_CONTROL_ID = 0x010F -SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID = 0x0110 -SwitchFields.GENERIC_SWITCH_ID = 0x000F -SwitchFields.ELECTRICAL_SENSOR_ID = 0x0510 +SwitchFields.DEVICE_TYPE_ID = { + AGGREGATOR = 0x000E, + DIMMABLE_PLUG_IN_UNIT = 0x010B, + ELECTRICAL_SENSOR = 0x0510, + GENERIC_SWITCH = 0x000F, + MOUNTED_ON_OFF_CONTROL = 0x010F, + MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, + ON_OFF_PLUG_IN_UNIT = 0x010A, + LIGHT = { + ON_OFF = 0x0100, + DIMMABLE = 0x0101, + COLOR_TEMPERATURE = 0x010C, + EXTENDED_COLOR = 0x010D, + }, + SWITCH = { + ON_OFF_LIGHT = 0x0103, + DIMMER = 0x0104, + COLOR_DIMMER = 0x0105, + }, +} SwitchFields.device_type_profile_map = { - [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = "light-binary", - [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = "light-level", - [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = "light-level-colorTemperature", - [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = "light-color-level", - [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = "plug-binary", - [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = "plug-level", - [SwitchFields.ON_OFF_SWITCH_ID] = "switch-binary", - [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = "switch-level", - [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = "switch-color-level", - [SwitchFields.MOUNTED_ON_OFF_CONTROL_ID] = "switch-binary", - [SwitchFields.MOUNTED_DIMMABLE_LOAD_CONTROL_ID] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = "light-binary", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = "light-level", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = "light-level-colorTemperature", + [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = "light-color-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = "switch-binary", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = "switch-color-level", + [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = "plug-binary", + [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", } @@ -123,6 +127,31 @@ SwitchFields.vendor_overrides = { } } +SwitchFields.switch_category_vendor_overrides = { + [0x1432] = -- Elko + {0x1000}, + [0x130A] = -- Eve + {0x005D, 0x0043}, + [0x1339] = -- GE + {0x007D, 0x0074, 0x0075}, + [0x1372] = -- Innovation Matters + {0x0002}, + [0x1021] = -- Legrand + {0x0005}, + [0x109B] = -- Leviton + {0x1001, 0x1000, 0x100B, 0x100E, 0x100C, 0x100D, 0x1009, 0x1003, 0x1004, 0x1002}, + [0x142B] = -- LeTianPai + {0x1004, 0x1003, 0x1002}, + [0x1509] = -- SmartSetup + {0x0004, 0x0001}, + [0x1321] = -- SONOFF + {0x000B, 0x000C, 0x000D}, + [0x147F] = -- U-Tec + {0x0004}, + [0x139C] = -- Zemismart + {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} +} + SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" @@ -180,16 +209,16 @@ SwitchFields.supported_capabilities = { } SwitchFields.device_type_attribute_map = { - [SwitchFields.ON_OFF_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.DIMMABLE_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.COLOR_TEMP_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -198,7 +227,7 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds }, - [SwitchFields.EXTENDED_COLOR_LIGHT_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -211,25 +240,25 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY }, - [SwitchFields.ON_OFF_PLUG_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.DIMMABLE_PLUG_DEVICE_TYPE_ID] = { + [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.ON_OFF_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = { clusters.OnOff.attributes.OnOff }, - [SwitchFields.ON_OFF_DIMMER_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, clusters.LevelControl.attributes.MinLevel }, - [SwitchFields.ON_OFF_COLOR_DIMMER_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -242,14 +271,14 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY }, - [SwitchFields.GENERIC_SWITCH_ID] = { + [SwitchFields.DEVICE_TYPE_ID.GENERIC_SWITCH] = { clusters.PowerSource.attributes.BatPercentRemaining, clusters.Switch.events.InitialPress, clusters.Switch.events.LongPress, clusters.Switch.events.ShortRelease, clusters.Switch.events.MultiPressComplete }, - [SwitchFields.ELECTRICAL_SENSOR_ID] = { + [SwitchFields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR] = { clusters.ElectricalPowerMeasurement.attributes.ActivePower, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua index 2558429654..2715f2e242 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua @@ -21,7 +21,8 @@ local log = require "log" local utils = {} function utils.tbl_contains(array, value) - for _, element in ipairs(array) do + if value == nil then return false end + for _, element in pairs(array or {}) do if element == value then return true end @@ -82,6 +83,14 @@ function utils.check_field_name_updates(device) end end +function utils.check_switch_category_vendor_overrides(device) + for _, product_id in ipairs(fields.switch_category_vendor_overrides[device.manufacturer_info.vendor_id] or {}) do + if device.manufacturer_info.product_id == product_id then + return true + end + end +end + --- device_type_supports_button_switch_combination helper function used to check --- whether the device type for an endpoint is currently supported by a profile for --- combination button/switch devices. @@ -89,10 +98,29 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i if utils.get_product_override_field(device, "ignore_combo_switch_button") then return false end - local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DIMMABLE_LIGHT_DEVICE_TYPE_ID) + local dimmable_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE) return utils.tbl_contains(dimmable_eps, endpoint_id) end +-- Some devices report multiple device types which are a subset of +-- a superset device type (Ex. Dimmable Light is a superset of On/Off Light). +-- We should map to the largest superset device type supported. +-- This can be done by matching to the device type with the highest ID +function utils.find_max_subset_device_type(ep, device_type_set) + if ep.endpoint_id == 0 then return end -- EP-scoped device types not permitted on Root Node + local primary_dt_id = ep.device_types[1] and ep.device_types[1].device_type_id + if utils.tbl_contains(device_type_set, primary_dt_id) then + for _, dt in ipairs(ep.device_types) do + -- only device types in the subset should be considered. + if utils.tbl_contains(device_type_set, dt.device_type_id) then + primary_dt_id = math.max(primary_dt_id, dt.device_type_id) + end + end + return primary_dt_id + end + return nil +end + --- find_default_endpoint is a helper function to handle situations where --- device does not have endpoint ids in sequential order from 1 function utils.find_default_endpoint(device) @@ -128,9 +156,9 @@ function utils.find_default_endpoint(device) -- endpoint. If it is not a supported device type, return the first button endpoint as the -- default endpoint. if #switch_eps > 0 and #button_eps > 0 then - local main_endpoint = get_first_non_zero_endpoint(switch_eps) - if utils.device_type_supports_button_switch_combination(device, main_endpoint) then - return main_endpoint + local default_endpoint_id = get_first_non_zero_endpoint(switch_eps) + if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then + return default_endpoint_id else device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") return get_first_non_zero_endpoint(button_eps) @@ -163,6 +191,13 @@ function utils.find_child(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) end +function utils.get_endpoint_info(device, endpoint_id) + for _, ep in ipairs(device.endpoints) do + if ep.endpoint_id == endpoint_id then return ep end + end + return {} +end + -- Fallback handler for responses that dont have their own handler function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) @@ -193,7 +228,7 @@ function utils.create_multi_press_values_list(size, supportsHeld) end function utils.detect_bridge(device) - return #utils.get_endpoints_by_device_type(device, fields.AGGREGATOR_DEVICE_TYPE_ID) > 0 + return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end function utils.detect_matter_thing(device) From 0724305781777cb640a20700b03624c1242bcf91 Mon Sep 17 00:00:00 2001 From: Inovelli <37669481+InovelliUSA@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:12:56 -0700 Subject: [PATCH 229/449] WWSTCERT-8354 Add support for Inovelli mmWave switch VZM32-SN (#2228) * Add support for Inovelli mmWave switch * Fix for measurement unit being cm (not mm) * Configure illuminance reporting and fix p101-106 unit incorrect * adjusting lux reporting. remove p117 as mmwave param and updated its options * initializing values for occupancy, illuminance, and binding to occupancy cluster * add ability to reset energy meter * add ability to reset energy meter * adjusting some default parameters and adding ota image notify for firmware update process during certification * adding missing OTAUpgrade declaration * removing unused capability * turning on notification child device when color set * fix mmwave reset command * adding ota image select preference * making some modificastions requested by ST * using default illuminance handler * removing some preferences and changing illuminance calculation method * removing extra line and white space * Making some code more efficient and adding unit test files. * Fix linter errors * Fixing more linter errors * adding more unit tests for energy, power, and illuminance reporting * adding unit tests for occupancy. Fix some linter errors * small linter errors fix * adding more unit tests * Fix minor linter error * combining inovelli vzm31 and vzm32 drivers. Add test unit for vzm31 * removing unused files * fix linter errors * remove test unit file * add more unit tests --------- Co-authored-by: InovelliUSA --- .../zigbee-switch/fingerprints.yml | 5 + .../profiles/inovelli-vzm32-sn.yml | 355 +++++++++++++ .../SmartThings/zigbee-switch/src/init.lua | 5 +- .../src/inovelli-vzm31-sn/init.lua | 400 --------------- .../zigbee-switch/src/inovelli/common.lua | 45 ++ .../zigbee-switch/src/inovelli/init.lua | 406 +++++++++++++++ .../src/inovelli/vzm32-sn/init.lua | 91 ++++ .../src/test/test_inovelli-vzm31-sn.lua | 484 ------------------ .../src/test/test_inovelli_vzm31_sn.lua | 356 +++++++++++++ .../src/test/test_inovelli_vzm31_sn_child.lua | 357 +++++++++++++ .../test_inovelli_vzm31_sn_preferences.lua | 211 ++++++++ .../src/test/test_inovelli_vzm32_sn.lua | 461 +++++++++++++++++ .../src/test/test_inovelli_vzm32_sn_child.lua | 356 +++++++++++++ .../test_inovelli_vzm32_sn_preferences.lua | 174 +++++++ 14 files changed, 2820 insertions(+), 886 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml delete mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/common.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua delete mode 100755 drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index fc6ae4e5c7..cb3890125d 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2364,6 +2364,11 @@ zigbeeManufacturer: manufacturer: Inovelli model: VZM31-SN deviceProfileName: inovelli-vzm31-sn + - id: "Inovelli/VZM32-SN" + deviceLabel: "Inovelli mmWave Dimmer Blue Series" + manufacturer: Inovelli + model: VZM32-SN + deviceProfileName: inovelli-vzm32-sn - id: "LAISIAO/BATH" deviceLabel: Laisiao Bathroom Heater manufacturer: LAISIAO diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml new file mode 100644 index 0000000000..746890a15a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -0,0 +1,355 @@ +name: inovelli-vzm32-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: Switch + - id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter258" + title: "258. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: false + preferenceType: enumeration + definition: + options: + "0": "Dimmer (default)" + "1": "On/Off" + default: 0 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 126 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter9" + title: "9. Minimum Level" + description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 99 + default: 1 + - name: "parameter10" + title: "10. Maximum Level" + description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." + required: false + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter110" + title: "110. Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "0": "Factory Reset Module" + default: 3 + - name: "parameter112" + title: "112. mmWave Detection Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "114. mmWave Time Out" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 30 + - name: "parameter34" + title: "34. OTA Image Type" + description: "Which endpoint should the switch advertise for OTA update (Zigbee, mmWave, or both)." + required: true + preferenceType: enumeration + definition: + options: + "0": "Zigbee (default)" + "1": "mmWave" + "2": "Alternating" + default: 0 diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 28beb014f4..dfaba9e573 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -84,7 +84,8 @@ local zigbee_switch_driver_template = { capabilities.colorTemperature, capabilities.powerMeter, capabilities.energyMeter, - capabilities.motionSensor + capabilities.motionSensor, + capabilities.illuminanceMeasurement, }, sub_drivers = { lazy_load_if_possible("non_zigbee_devices"), @@ -112,7 +113,7 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("bad_on_off_data_type"), lazy_load_if_possible("robb"), lazy_load_if_possible("wallhero"), - lazy_load_if_possible("inovelli-vzm31-sn"), + lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi"), lazy_load_if_possible("frient") diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua deleted file mode 100644 index a0dae6db3f..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ /dev/null @@ -1,400 +0,0 @@ --- Copyright 2024 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 clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local utils = require "st.utils" -local st_device = require "st.device" -local data_types = require "st.zigbee.data_types" -local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" -local configurations = require "configurations" -local switch_utils = require "switch_utils" - -local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" - -local INOVELLI_VZM31_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM31-SN" } -} - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_SCENE_ID =0x00 -local MFG_CODE = 0x122F - -local preference_map = { - parameter258 = {parameter_number = 258, size = data_types.Boolean}, - parameter22 = {parameter_number = 22, size = data_types.Uint8}, - parameter52 = {parameter_number = 52, size = data_types.Boolean}, - parameter1 = {parameter_number = 1, size = data_types.Uint8}, - parameter2 = {parameter_number = 2, size = data_types.Uint8}, - parameter3 = {parameter_number = 3, size = data_types.Uint8}, - parameter4 = {parameter_number = 4, size = data_types.Uint8}, - parameter9 = {parameter_number = 9, size = data_types.Uint8}, - parameter10 = {parameter_number = 10, size = data_types.Uint8}, - parameter11 = {parameter_number = 11, size = data_types.Boolean}, - parameter15 = {parameter_number = 15, size = data_types.Uint8}, - parameter17 = {parameter_number = 17, size = data_types.Uint8}, - parameter95 = {parameter_number = 95, size = data_types.Uint8}, - parameter96 = {parameter_number = 96, size = data_types.Uint8}, - parameter97 = {parameter_number = 97, size = data_types.Uint8}, - parameter98 = {parameter_number = 98, size = data_types.Uint8}, -} - -local preferences_to_numeric_value = function(new_value) - local numeric = tonumber(new_value) - if numeric == nil then -- in case the value is Boolean - numeric = new_value and 1 or 0 - end - return numeric -end - -local preferences_calculate_parameter = function(new_value, type, number) - if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then - if new_value == 101 then - return 255 - else - return utils.round(new_value / 100 * 254) - end - else - return new_value - end -end - -local is_inovelli_vzm31_sn = function(opts, driver, device) - for _, fingerprint in ipairs(INOVELLI_VZM31_SN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("inovelli-vzm31-sn") - return true, subdriver - end - end - return false -end - -local function to_boolean(value) - if value == 0 or value =="0" then - return false - else - return true - end -end - -local map_key_attribute_to_capability = { - [0x00] = capabilities.button.button.pushed, - [0x01] = capabilities.button.button.held, - [0x02] = capabilities.button.button.down_hold, - [0x03] = capabilities.button.button.pushed_2x, - [0x04] = capabilities.button.button.pushed_3x, - [0x05] = capabilities.button.button.pushed_4x, - [0x06] = capabilities.button.button.pushed_5x, -} - -local function button_to_component(buttonId) - if buttonId > 0 then - return string.format("button%d", buttonId) - end -end - -local function scene_handler(driver, device, zb_rx) - local bytes = zb_rx.body.zcl_body.body_bytes - local button_number = bytes:byte(1) - local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - local comp = device.profile.components[button_to_component(button_number)] - if comp ~= nil then - device:emit_component_event(comp, event) - end -end - -local function add_child(driver,parent,profile,child_type) - local child_metadata = { - type = "EDGE_CHILD", - label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), - profile = profile, - parent_device_id = parent.id, - parent_assigned_child_key = child_type, - vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) - } - driver:try_create_device(child_metadata) -end - -local function info_changed(driver, device, event, args) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - local time_diff = 3 - local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) - if last_clock_set_time ~= nil then - time_diff = os.difftime(os.time(), last_clock_set_time) - end - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) - - if time_diff > 2 then - local preferences = preference_map - if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then - if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') - end - end - for id, value in pairs(device.preferences) do - if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then - local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) - - if(preferences[id].size == data_types.Boolean) then - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) - else - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - end - end -end - -local do_configure = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:refresh() - device:configure() - - device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. - - -- Retrieve Neutral Setting "Parameter 21" - device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - - -- Additional one time configuration - if device:supports_capability(capabilities.powerMeter) then - -- Divisor and multipler for PowerMeter - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - end - - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end - end -end - -local device_init = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time()) - if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil and device:supports_capability(capabilities.switchLevel)then - device:emit_event(capabilities.switchLevel.level(0)) - end - if device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) == nil and device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power(0)) - end - if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then - device:emit_event(capabilities.energyMeter.energy(0)) - end - - for _, component in pairs(device.profile.components) do - if string.find(component.id, "button") ~= nil then - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.supportedButtonValues.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, - { visibility = { displayed = false } } - ) - ) - end - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.numberOfButtons.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - else - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl, capabilities.colorControl.hue.NAME, capabilities.colorControl.hue(1)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorControl, capabilities.colorControl.saturation.NAME, capabilities.colorControl.saturation(1)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperature.NAME, capabilities.colorTemperature.colorTemperature(6500)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switchLevel, capabilities.switchLevel.level.NAME, capabilities.switchLevel.level(100)) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.switch, capabilities.switch.switch.NAME, capabilities.switch.switch("off")) - end -end - -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - -local function huePercentToValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function getNotificationValue(device, 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 - notificationValue = notificationValue + (effect*16777216) - notificationValue = notificationValue + (huePercentToValue(value or color)*65536) - notificationValue = notificationValue + (level*256) - notificationValue = notificationValue + (255*1) - return notificationValue -end - -local function on_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.On(device)) - else - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - 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))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function off_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.Off(device)) - else - device:emit_event(capabilities.switch.switch("off")) - local dev = device:get_parent_device() - 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(0,4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function switch_level_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) - else - 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() - 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))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function set_color_temperature(driver, device, command) - device:emit_event(capabilities.colorControl.hue(100)) - device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) - local dev = device:get_parent_device() - 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))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local function set_color(driver, device, command) - device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) - device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) - local dev = device:get_parent_device() - 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))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local inovelli_vzm31_sn = { - NAME = "inovelli vzm31-sn handler", - lifecycle_handlers = { - doConfigure = do_configure, - init = configurations.power_reconfig_wrapper(device_init), - infoChanged = info_changed - }, - zigbee_handlers = { - attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - } - }, - cluster = { - [PRIVATE_CLUSTER_ID] = { - [PRIVATE_CMD_SCENE_ID] = scene_handler, - } - } - }, - capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = on_handler, - [capabilities.switch.commands.off.NAME] = off_handler, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler - }, - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - }, - [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature - } - }, - can_handle = is_inovelli_vzm31_sn -} - -return inovelli_vzm31_sn diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua new file mode 100644 index 0000000000..c80d0deca9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -0,0 +1,45 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade + +local M = {} + +-- Sends a generic configure for Inovelli devices (all models): +-- - device:configure +-- - send OTA ImageNotify +-- - bind PRIVATE cluster for button presses +-- - read metering/electrical measurement divisors/multipliers +function M.base_device_configure(driver, device, private_cluster_id, mfg_code) + device:configure() + -- OTA Image Notify (generic for all devices) + local PAYLOAD_TYPE = 0x00 + local QUERY_JITTER = 100 + local IMAGE_TYPE = 0xFFFF + local NEW_VERSION = 0xFFFFFFFF + device:send(OTAUpgrade.commands.ImageNotify(device, PAYLOAD_TYPE, QUERY_JITTER, mfg_code, IMAGE_TYPE, NEW_VERSION)) + + -- Bind for button presses on manufacturer private cluster + device:send(device_management.build_bind_request(device, private_cluster_id, driver.environment_info.hub_zigbee_eui, 2)) + + -- Read divisors/multipliers for power/energy reporting + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) +end + +return M \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua new file mode 100644 index 0000000000..253dcca86c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -0,0 +1,406 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local st_device = require "st.device" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local inovelli_common = require "inovelli.common" + +-- Load VZM32-only dependencies (handlers will check device type) +local OccupancySensing = clusters.OccupancySensing + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM31-SN" }, + { mfr = "Inovelli", model = "VZM32-SN" } +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 +local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_ENERGY_RESET_ID = 0x02 +local PRIVATE_CMD_SCENE_ID = 0x00 +local PRIVATE_CMD_MMWAVE_ID = 0x00 +local MFG_CODE = 0x122F + +-- Base preferences shared by all models +local base_preference_map = { + parameter258 = {parameter_number = 258, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter52 = {parameter_number = 52, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter1 = {parameter_number = 1, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter2 = {parameter_number = 2, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter3 = {parameter_number = 3, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter4 = {parameter_number = 4, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter15 = {parameter_number = 15, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter95 = {parameter_number = 95, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter96 = {parameter_number = 96, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter97 = {parameter_number = 97, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter98 = {parameter_number = 98, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, +} + +-- Model-specific overrides/additions +local model_preference_overrides = { + ["VZM31-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + }, + ["VZM32-SN"] = { + parameter34 = {parameter_number = 34, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter101 = {parameter_number = 101, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter102 = {parameter_number = 102, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter103 = {parameter_number = 103, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter104 = {parameter_number = 104, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter105 = {parameter_number = 105, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter106 = {parameter_number = 106, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter110 = {parameter_number = 110, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter111 = {parameter_number = 111, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter112 = {parameter_number = 112, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter113 = {parameter_number = 113, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter114 = {parameter_number = 114, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter115 = {parameter_number = 115, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_ID}, + } +} + +local function get_preference_map_for_device(device) + -- shallow copy base + local merged = {} + for k, v in pairs(base_preference_map) do merged[k] = v end + -- merge model-specific + local model = device and device:get_model() or nil + local override = model and model_preference_overrides[model] or nil + if override then + for k, v in pairs(override) do merged[k] = v end + end + return merged +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then + if new_value == 101 then + return 255 + else + return utils.round(new_value / 100 * 254) + end + else + return new_value + end +end + +local can_handle_inovelli = function(opts, driver, device) + for _, fp in ipairs(INOVELLI_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end + +local function to_boolean(value) + if value == 0 or value == "0" then + return false + else + return true + end +end + +local map_key_attribute_to_capability = { + [0x00] = capabilities.button.button.pushed, + [0x01] = capabilities.button.button.held, + [0x02] = capabilities.button.button.down_hold, + [0x03] = capabilities.button.button.pushed_2x, + [0x04] = capabilities.button.button.pushed_3x, + [0x05] = capabilities.button.button.pushed_4x, + [0x06] = capabilities.button.button.pushed_5x, +} + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function scene_handler(driver, device, zb_rx) + local bytes = zb_rx.body.zcl_body.body_bytes + local button_number = bytes:byte(1) + local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] + local additional_fields = { state_change = true } + local event = capability_attribute and capability_attribute(additional_fields) or nil + local comp = device.profile.components[button_to_component(button_number)] + if comp ~= nil and event ~= nil then + device:emit_component_event(comp, event) + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then time_diff = os.difftime(os.time(), last_clock_set_time) end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = get_preference_map_for_device(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') + end + end + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + if(preferences[id].size == data_types.Boolean) then + new_parameter_value = to_boolean(new_parameter_value) + end + if id == "parameter111" then + device:send(cluster_base.build_manufacturer_specific_command( + device, + PRIVATE_CLUSTER_MMWAVE_ID, + PRIVATE_CMD_MMWAVE_ID, + MFG_CODE, + utils.serialize_int(new_parameter_value,1,false,false))) + else + device:send(cluster_base.write_manufacturer_specific_attribute(device, preferences[id].cluster, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) + end + end + end + end + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:refresh() + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + else + device:configure() + end +end + +local function energy_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 100 + device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) +end + +local function power_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 10 + device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) +end + +local function huePercentToValue(value) + if value <= 2 then return 0 + elseif value >= 98 then return 255 + else return utils.round(value / 100 * 255) end +end + +local function getNotificationValue(device, 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 + notificationValue = notificationValue + (effect*16777216) + notificationValue = notificationValue + (huePercentToValue(value or color)*65536) + notificationValue = notificationValue + (level*256) + notificationValue = notificationValue + (255*1) + return notificationValue +end + +local function on_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.On(device)) + else + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + 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))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + + local function off_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.Off(device)) + else + device:emit_event(capabilities.switch.switch("off")) + local dev = device:get_parent_device() + 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(0,4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function switch_level_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) + else + 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() + 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))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function set_color_temperature(driver, device, command) + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + 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))) + end + device.thread:call_with_delay(1,send_configuration) + end + + local function set_color(driver, device, command) + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + 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))) + end + device.thread:call_with_delay(1,send_configuration) + end + +local function occupancy_attr_handler(driver, device, occupancy, zb_rx) + device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + +local function handle_resetEnergyMeter(self, device) + device:send(cluster_base.build_manufacturer_specific_command(device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_ENERGY_RESET_ID, MFG_CODE, utils.serialize_int(0,1,false,false))) + device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) +end + +local inovelli = { + NAME = "inovelli combined handler", + lifecycle_handlers = { + doConfigure = device_configure, + infoChanged = info_changed, + added = device_added, + }, + zigbee_handlers = { + attr = { + [clusters.SimpleMetering.ID] = { + [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, + [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler + }, + [clusters.ElectricalMeasurement.ID] = { + [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler + }, + [OccupancySensing.ID] = { + [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler + }, + }, + cluster = { + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_CMD_SCENE_ID] = scene_handler, + } + } + }, + sub_drivers = { + require("inovelli/vzm32-sn"), + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, + } + }, + can_handle = can_handle_inovelli +} + +return inovelli \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua new file mode 100644 index 0000000000..2be05dad3e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -0,0 +1,91 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local st_device = require "st.device" +local device_management = require "st.zigbee.device_management" +local inovelli_common = require "inovelli.common" + +local OccupancySensing = clusters.OccupancySensing + +local INOVELLI_VZM32_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM32-SN" }, +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +local function can_handle_inovelli_vzm32_sn(opts, driver, device) + for _, fp in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + return true + end + end + return false +end + +local function configure_illuminance_reporting(device) + local min_lux_change = 15 + local value = math.floor(10000 * math.log(min_lux_change, 10) + 1) + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(device, 10, 600, value)) +end + +local function refresh_handler(driver, device, command) + if device.network_type ~= device.NETWORK_TYPE_CHILD then + device:refresh() + device:send(OccupancySensing.attributes.Occupancy:read(device)) + else + device:refresh() + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + refresh_handler(driver, device, {}) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + device:send(device_management.build_bind_request(device, OccupancySensing.ID, driver.environment_info.hub_zigbee_eui)) + configure_illuminance_reporting(device) + else + device:configure() + end +end + +local vzm32_sn = { + NAME = "inovelli vzm32-sn device-specific", + can_handle = can_handle_inovelli_vzm32_sn, + lifecycle_handlers = { + added = device_added, + doConfigure = device_configure, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler, + } + } +} + +return vzm32_sn \ No newline at end of file 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 deleted file mode 100755 index 41ae8a049b..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua +++ /dev/null @@ -1,484 +0,0 @@ --- Copyright 2025 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 test = require "integration_test" -local clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local BasicCluster = clusters.Basic -local OnOffCluster = clusters.OnOff -local LevelCluster = clusters.Level -local SimpleMeteringCluster = clusters.SimpleMetering -local ElectricalMeasurementCluster = clusters.ElectricalMeasurement -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local data_types = require "st.zigbee.data_types" -local utils = require "st.utils" - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_SCENE_ID =0x00 -local MFG_CODE = 0x122F - -local parent_profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml") -local child_profile = t_utils.get_profile_definition("rgbw-bulb-2700K-6500K.yml") - -local mock_device = test.mock_device.build_test_zigbee_device({ - label = "Inovelli 2-in-1 Blue Series", - profile = parent_profile, - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0000, 0x0006, 0x0008, 0x0702, 0x0B04 }, - }, - [2] = { - id = 2, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - [3] = { - id = 3, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - [4] = { - id = 4, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - }, - fingerprinted_endpoint_id = 0x01 -}) - -local mock_first_child = test.mock_device.build_test_child_device({ - profile = child_profile, - 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() - mock_device:set_field("_configuration_version", 1, {persist = true}) - test.mock_device.add_test_device(mock_device) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerMeter.power(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - - test.socket.zigbee:__expect_send({mock_device.id, BasicCluster.attributes.SWBuildID:read(mock_device)}) - - test.mock_device.add_test_device(mock_first_child) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(1))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(1))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(100))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) - -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "lifecycle configure event should configure device", - function () - 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, - LevelCluster.attributes.CurrentLevel:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.Multiplier:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.Divisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x02) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x03) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x04) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(3) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(4) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - LevelCluster.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) - }) - - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - BasicCluster.attributes.SWBuildID:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - ElectricalMeasurementCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - SimpleMeteringCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - LevelCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 1):to_endpoint(1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 2):to_endpoint(2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 3):to_endpoint(3) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 4):to_endpoint(4) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - PRIVATE_CLUSTER_ID, 2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 21, MFG_CODE) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) - }) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "parameter258 in infochanged", - function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ - preferences = { ["parameter258"] = "0" } - })) - test.mock_time.advance_time(3) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 258, - MFG_CODE, data_types.Boolean, false) }) - test.socket.zigbee:__expect_send({ mock_device.id, - BasicCluster.attributes.SWBuildID:read(mock_device) }) - end -) - -test.register_coroutine_test( - "parameter22 in infochanged", - function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ - preferences = { ["parameter22"] = "0" } - })) - test.mock_time.advance_time(3) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 22, - MFG_CODE, data_types.Uint8, 0) }) - test.socket.zigbee:__expect_send({ mock_device.id, - BasicCluster.attributes.SWBuildID:read(mock_device) }) - end -) - - -test.register_message_test( - "Capability on command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.On(mock_device) } - } - } -) - -test.register_coroutine_test( - "Capability on command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "on", args = {}}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16803071,4,false,false)) }) - end -) - -test.register_message_test( - "Capability off command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.Off(mock_device) } - } - } -) - -test.register_coroutine_test( - "Capability on command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "off", args = {}}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(0,4,false,false)) }) - end -) - - -test.register_message_test( - "Capability setLevel command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, LevelCluster.server.commands.MoveToLevelWithOnOff(mock_device, - math.floor(57 * 0xFE / 100), - 0) } - } - } -) - -test.register_coroutine_test( - "Capability setLevel command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(57))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16792063,4,false,false)) }) - end -) - - -test.register_coroutine_test( - "Capability setColorTemperature command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = { 1800 }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(100))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(33514751,4,false,false)) }) - end -) - -test.register_coroutine_test( - "Capability setColor command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(50))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(50))) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(25191679,4,false,false)) }) - end -) - -local ENDPOINT = 0x01 -local FRAME_CTRL = 0x1D -local PROFILE_ID = 0x0104 - -local build_scene_message = function(device, payload) - local message = zigbee_test_utils.build_custom_command_id( - device, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_SCENE_ID, - MFG_CODE, - payload, - ENDPOINT - ) - - message.body.zcl_header.frame_ctrl.value = FRAME_CTRL - message.address_header.profile.value = PROFILE_ID - - return message -end - -test.register_coroutine_test( - "Reported private cluster should be handled", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - build_scene_message(mock_device, "\x01\x01") - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) - end -) - -test.register_coroutine_test( - "Handle Power meter", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 60) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 6.0, unit = "W" })) - ) - - test.socket.zigbee:__queue_receive({ - mock_device.id, - ElectricalMeasurementCluster.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 -) - -test.register_coroutine_test( - "Handle Energy meter", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 600) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 6.0, unit = "kWh" })) - ) - end -) - -test.run_registered_tests() 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 new file mode 100644 index 0000000000..65a3084022 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -0,0 +1,356 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" + +-- Inovelli VZM31-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM31_SN_MODEL = "VZM31-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM31_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm31_sn) +end +test.set_test_init_function(test_init) + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm31_sn) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm31_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm31_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm31_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + } +) + +-- Test power meter from SimpleMetering +test.register_message_test( + "Power meter from SimpleMetering should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm31_sn, 1500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_message_test( + "Power meter from ElectricalMeasurement should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + } + } +) + +-- Test energy meter +test.register_message_test( + "Energy meter should emit energy events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + } + } +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm31_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.register_coroutine_test( + "doConfigure runs base config (VZM31)", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() + 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 new file mode 100644 index 0000000000..b6c6c86a38 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua @@ -0,0 +1,357 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() + 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 new file mode 100644 index 0000000000..a9e8850b7d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua @@ -0,0 +1,211 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM31-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm31_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter9 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 9, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM31-only) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM31-only) +test.register_coroutine_test( + "parameter17 preference should send configuration command", + function() + local new_param_value = 5 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter17 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 17, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM31-only) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 22, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm31_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM31-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm31_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +test.run_registered_tests() + 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 new file mode 100644 index 0000000000..452b98454d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -0,0 +1,461 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" + +local OnOff = clusters.OnOff +local Level = clusters.Level + +-- Inovelli VZM32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM32_SN_MODEL = "VZM32-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM32_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300, 0x0406} -- OnOff, Level, ColorControl, OccupancySensing + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm32_sn) +end +test.set_test_init_function(test_init) + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should send read commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm32_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + } +) + +-- Test illuminance measurement +test.register_message_test( + "Illuminance measurement should emit illuminance events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm32_sn, 11271) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({value = 13})) + } + } +) + +-- Test motion sensor active +test.register_message_test( + "Motion sensor active should emit motion active event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x01) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + +-- Test motion sensor inactive +test.register_message_test( + "Motion sensor inactive should emit motion inactive event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x00) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + +-- Test power meter from SimpleMetering +test.register_message_test( + "Power meter from SimpleMetering should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm32_sn, 1500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_message_test( + "Power meter from ElectricalMeasurement should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + } + } +) + +-- Test energy meter +test.register_message_test( + "Energy meter should emit energy events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 50000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + } + } +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm32_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.register_coroutine_test( + "doConfigure runs base + VZM32 extras", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..d660e8c501 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -0,0 +1,356 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..9bb0a07147 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -0,0 +1,174 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM32-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm32_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter9 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 9, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM32-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm32_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +-- Test parameter101 preference change +test.register_coroutine_test( + "parameter101 preference should send configuration command", + function() + local new_param_value = 200 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + local expected_command = cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC32, -- PRIVATE_CLUSTER_ID + 101, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Int16, + new_param_value + ) + + print("=== DEBUG: Expected command ===") + print("Command type:", type(expected_command)) + print("Command:", expected_command) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + expected_command + }) + end +) + +test.run_registered_tests() \ No newline at end of file From 46c5e2d9c58ab0663d87ee980f7f24426bddd8ce Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 29 Oct 2025 11:07:12 -0700 Subject: [PATCH 230/449] Merge pull request #2508 from SmartThingsCommunity/new_device/WWSTCERT-8637 WWSTCERT-8637 NodOn Zigbee Multifunction Relay Switch --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index e449e1f194..fc6ae4e5c7 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2369,6 +2369,12 @@ zigbeeManufacturer: manufacturer: LAISIAO model: yuba deviceProfileName: switch-smart-bath-heater-laisiao + # NodOn + - id: "NodOn/SIN-4-1-20" + deviceLabel: Zigbee Multifunction Relay Switch + manufacturer: NodOn + model: SIN-4-1-20 + deviceProfileName: basic-switch zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From 5f6faa7838faa2d8f9fef662982b0924a7d2829d Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 29 Oct 2025 11:29:25 -0700 Subject: [PATCH 231/449] Merge pull request #2509 from SmartThingsCommunity/new_device/WWSTCERT-8659 WWSTCERT-8637 NodOn Zigbee Roller Shutter Relay Switch --- drivers/SmartThings/zigbee-window-treatment/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index 808add60ff..c677cbce4b 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -128,6 +128,11 @@ zigbeeManufacturer: manufacturer: VIVIDSTORM model: VWSDSTUST120H deviceProfileName: projector-screen-VWSDSTUST120H + - id: "NodOn/SIN-4-RS-20" + deviceLabel: Zigbee Roller Shutter Relay Switch + manufacturer: NodOn + model: SIN-4-RS-20 + deviceProfileName: window-treatment-profile zigbeeGeneric: - id: "genericShade" From 056ee617f8808810a0ba3d4f9fbd2965b621791f Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 10 Oct 2025 12:22:20 -0500 Subject: [PATCH 232/449] Enable use of lazy_load_subdriver_v2 in zigbee switch This is a new feature in api v16 that allows for more memory savings by more efficiently lazily loading the subdriver. Subdrivers must have their can_handle function and their own subdrivers split out into separate modules to work with the new lazy loading api. --- .../src/aqara-light/can_handle.lua | 14 ++ .../zigbee-switch/src/aqara-light/init.lua | 17 +-- .../zigbee-switch/src/aqara/can_handle.lua | 10 ++ .../zigbee-switch/src/aqara/fingerprints.lua | 19 +++ .../zigbee-switch/src/aqara/init.lua | 31 +---- .../src/aqara/multi-switch/can_handle.lua | 9 ++ .../src/aqara/multi-switch/fingerprints.lua | 11 ++ .../src/aqara/multi-switch/init.lua | 24 +--- .../zigbee-switch/src/aqara/sub_drivers.lua | 6 + .../src/aqara/version/can_handle.lua | 10 ++ .../zigbee-switch/src/aqara/version/init.lua | 5 +- .../src/bad_on_off_data_type/can_handle.lua | 21 +++ .../src/bad_on_off_data_type/init.lua | 24 +--- .../zigbee-switch/src/ezex/can_handle.lua | 14 ++ .../zigbee-switch/src/ezex/init.lua | 17 +-- .../zigbee-switch/src/frient/can_handle.lua | 11 ++ .../zigbee-switch/src/frient/fingerprints.lua | 14 ++ .../zigbee-switch/src/frient/init.lua | 29 +--- .../src/ge-link-bulb/can_handle.lua | 19 +++ .../zigbee-switch/src/ge-link-bulb/init.lua | 22 +-- .../zigbee-switch/src/hanssem/can_handle.lua | 10 ++ .../src/hanssem/fingerprints.lua | 8 ++ .../zigbee-switch/src/hanssem/init.lua | 24 +--- .../src/ikea-xy-color-bulb/can_handle.lua | 14 ++ .../src/ikea-xy-color-bulb/init.lua | 19 +-- .../SmartThings/zigbee-switch/src/init.lua | 13 +- .../zigbee-switch/src/inovelli/can_handle.lua | 14 ++ .../zigbee-switch/src/inovelli/init.lua | 23 +--- .../src/inovelli/sub_drivers.lua | 5 + .../src/inovelli/vzm32-sn/can_handle.lua | 13 ++ .../src/inovelli/vzm32-sn/init.lua | 16 +-- .../zigbee-switch/src/jasco/can_handle.lua | 15 +++ .../zigbee-switch/src/jasco/init.lua | 18 +-- .../zigbee-switch/src/laisiao/can_handle.lua | 13 ++ .../zigbee-switch/src/laisiao/init.lua | 14 +- .../zigbee-switch/src/lazy_load_subdriver.lua | 12 ++ .../src/multi-switch-no-master/can_handle.lua | 10 ++ .../multi-switch-no-master/fingerprints.lua | 43 ++++++ .../src/multi-switch-no-master/init.lua | 59 +------- .../src/non_zigbee_devices/can_handle.lua | 8 ++ .../src/non_zigbee_devices/init.lua | 11 +- .../zigbee-switch/src/rexense/can_handle.lua | 13 ++ .../zigbee-switch/src/rexense/init.lua | 16 +-- .../zigbee-switch/src/rgb-bulb/can_handle.lua | 20 +++ .../zigbee-switch/src/rgb-bulb/init.lua | 21 +-- .../src/rgbw-bulb/can_handle.lua | 10 ++ .../src/rgbw-bulb/fingerprints.lua | 71 ++++++++++ .../zigbee-switch/src/rgbw-bulb/init.lua | 84 +----------- .../zigbee-switch/src/robb/can_handle.lua | 14 ++ .../zigbee-switch/src/robb/init.lua | 17 +-- .../src/sinope-dimmer/can_handle.lua | 9 ++ .../zigbee-switch/src/sinope-dimmer/init.lua | 10 +- .../zigbee-switch/src/sinope/can_handle.lua | 9 ++ .../zigbee-switch/src/sinope/init.lua | 10 +- .../src/tuya-multi/can_handle.lua | 18 +++ .../zigbee-switch/src/tuya-multi/init.lua | 24 +--- .../zigbee-switch/src/wallhero/can_handle.lua | 11 ++ .../src/wallhero/fingerprints.lua | 8 ++ .../zigbee-switch/src/wallhero/init.lua | 22 +-- .../src/white-color-temp-bulb/can_handle.lua | 10 ++ .../duragreen/can_handle.lua | 15 +++ .../white-color-temp-bulb/duragreen/init.lua | 14 +- .../white-color-temp-bulb/fingerprints.lua | 96 +++++++++++++ .../src/white-color-temp-bulb/init.lua | 113 +--------------- .../src/white-color-temp-bulb/sub_drivers.lua | 4 + .../zigbee-dimmer-power-energy/can_handle.lua | 13 ++ .../src/zigbee-dimmer-power-energy/init.lua | 16 +-- .../src/zigbee-dimming-light/can_handle.lua | 10 ++ .../src/zigbee-dimming-light/fingerprints.lua | 33 +++++ .../src/zigbee-dimming-light/init.lua | 51 +------ .../osram-iqbr30/can_handle.lua | 7 + .../osram-iqbr30/init.lua | 6 +- .../src/zigbee-dimming-light/sub_drivers.lua | 6 + .../zll-dimmer/can_handle.lua | 9 ++ .../zll-dimmer/fingerprints.lua | 8 ++ .../zigbee-dimming-light/zll-dimmer/init.lua | 20 +-- .../can_handle.lua | 13 ++ .../src/zigbee-dual-metering-switch/init.lua | 16 +-- .../can_handle.lua | 9 ++ .../init.lua | 10 +- .../aurora-relay/can_handle.lua | 14 ++ .../zigbee-switch-power/aurora-relay/init.lua | 17 +-- .../src/zigbee-switch-power/can_handle.lua | 10 ++ .../src/zigbee-switch-power/fingerprints.lua | 15 +++ .../src/zigbee-switch-power/init.lua | 33 +---- .../src/zigbee-switch-power/sub_drivers.lua | 6 + .../zigbee-switch-power/vimar/can_handle.lua | 12 ++ .../src/zigbee-switch-power/vimar/init.lua | 15 +-- .../src/zll-dimmer-bulb/can_handle.lua | 10 ++ .../src/zll-dimmer-bulb/fingerprints.lua | 114 ++++++++++++++++ .../src/zll-dimmer-bulb/init.lua | 127 +----------------- .../src/zll-polling/can_handle.lua | 11 ++ .../zigbee-switch/src/zll-polling/init.lua | 15 +-- 93 files changed, 986 insertions(+), 968 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua new file mode 100644 index 0000000000..1900934725 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua @@ -0,0 +1,14 @@ +return function(opts, driver, device) + local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.light.acn004" }, + { mfr = "Aqara", model = "lumi.light.acn014" } + } + + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("aqara-light") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 3009cb6d0c..ec6e178f42 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -12,21 +12,6 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.light.acn004" }, - { mfr = "Aqara", model = "lumi.light.acn014" } -} - -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("aqara-light") - return true, subdriver - end - end - return false -end - local function do_refresh(self, device) device:send(OnOff.attributes.OnOff:read(device)) device:send(Level.attributes.CurrentLevel:read(device)) @@ -65,7 +50,7 @@ local aqara_light_handler = { [capabilities.switchLevel.commands.setLevel.NAME] = set_level_handler } }, - can_handle = is_aqara_products + can_handle = require("aqara-light.can_handle"), } return aqara_light_handler diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua new file mode 100644 index 0000000000..312e3a7e08 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("aqara") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..837b3243b8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua @@ -0,0 +1,19 @@ +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" } + } diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua index 6558f7f282..c4acb83a94 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua @@ -24,26 +24,6 @@ local ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID = 0x000A local LAST_REPORT_TIME = "LAST_REPORT_TIME" local PRIVATE_MODE = "PRIVATE_MODE" -local FINGERPRINTS = { - { 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" } -} - local preference_map = { ["stse.restorePowerState"] = { cluster_id = PRIVATE_CLUSTER_ID, @@ -137,15 +117,6 @@ local preference_map = { }, } -local function is_aqara_products(opts, driver, device) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("aqara") - return true, subdriver - end - end - return false -end local function private_mode_handler(driver, device, value, zb_rx) device:set_field(PRIVATE_MODE, value.value, { persist = true }) @@ -286,7 +257,7 @@ local aqara_switch_handler = { require("aqara.version"), require("aqara.multi-switch") }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_switch_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 new file mode 100644 index 0000000000..26eb425583 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua @@ -0,0 +1,9 @@ +return function(opts, driver, device) + local FINGERPRINTS = require("aqara.multi-switch.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara.multi-switch") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua new file mode 100644 index 0000000000..e8bfe86866 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua @@ -0,0 +1,11 @@ +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" } +} 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 4e54b67e2f..cf143091e7 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -8,27 +8,7 @@ local switch_utils = require "switch_utils" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F - -local FINGERPRINTS = { - { 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" } -} - -local function is_aqara_products(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 FINGERPRINTS = require("aqara.multi-switch.fingerprints") local function get_children_amount(device) for _, fingerprint in ipairs(FINGERPRINTS) do @@ -106,7 +86,7 @@ local aqara_multi_switch_handler = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = is_aqara_products + can_handle = require("aqara.multi-switch.can_handle"), } return aqara_multi_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..cf4fec5466 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua @@ -0,0 +1,6 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("aqara.multi-switch"), + lazy_load("aqara.version"), +} diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua new file mode 100644 index 0000000000..780b53a434 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua @@ -0,0 +1,10 @@ +return function (opts, driver, device) + local PRIVATE_MODE = "PRIVATE_MODE" + local private_mode = device:get_field(PRIVATE_MODE) or 0 + local res = private_mode == 1 + if res then + return res, require("aqara.version") + else + return res + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua index 01767da106..6e3b5ab125 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua @@ -102,10 +102,7 @@ local aqara_switch_version_handler = { } } }, - can_handle = function (opts, driver, device) - local private_mode = device:get_field(PRIVATE_MODE) or 0 - return private_mode == 1 - end + can_handle = require("aqara.version.can_handle") } return aqara_switch_version_handler diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua new file mode 100644 index 0000000000..7b9510968d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua @@ -0,0 +1,21 @@ + +-- There are reports of at least one device (SONOFF 01MINIZB) which occasionally +-- reports this value as an Int8, rather than a Boolean, as per the spec +return function(opts, driver, device, zb_rx, ...) + local zcl_clusters = require "st.zigbee.zcl.clusters" + local data_types = require "st.zigbee.data_types" + local can_handle = opts.dispatcher_class == "ZigbeeMessageDispatcher" and + device:get_manufacturer() == "SONOFF" and + zb_rx.body and + zb_rx.body.zcl_body and + zb_rx.body.zcl_body.attr_records and + zb_rx.address_header.cluster.value == zcl_clusters.OnOff.ID and + zb_rx.body.zcl_body.attr_records[1].attr_id.value == zcl_clusters.OnOff.attributes.OnOff.ID and + zb_rx.body.zcl_body.attr_records[1].data_type.value ~= data_types.Boolean.ID + if can_handle then + local subdriver = require("bad_on_off_data_type") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua index ee0c82977e..863d1e4ec4 100644 --- a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua @@ -13,28 +13,8 @@ -- limitations under the License. local zcl_clusters = require "st.zigbee.zcl.clusters" -local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" --- There are reports of at least one device (SONOFF 01MINIZB) which occasionally --- reports this value as an Int8, rather than a Boolean, as per the spec -local function incorrect_data_type_detected(opts, driver, device, zb_rx, ...) - local can_handle = opts.dispatcher_class == "ZigbeeMessageDispatcher" and - device:get_manufacturer() == "SONOFF" and - zb_rx.body and - zb_rx.body.zcl_body and - zb_rx.body.zcl_body.attr_records and - zb_rx.address_header.cluster.value == zcl_clusters.OnOff.ID and - zb_rx.body.zcl_body.attr_records[1].attr_id.value == zcl_clusters.OnOff.attributes.OnOff.ID and - zb_rx.body.zcl_body.attr_records[1].data_type.value ~= data_types.Boolean.ID - if can_handle then - local subdriver = require("bad_on_off_data_type") - return true, subdriver - else - return false - 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 == 0 and attr.off() or attr.on()) @@ -49,7 +29,7 @@ local bad_on_off_data_type = { } } }, - can_handle = incorrect_data_type_detected + can_handle = require("bad_on_off_data_type.can_handle"), } -return bad_on_off_data_type \ No newline at end of file +return bad_on_off_data_type diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua new file mode 100644 index 0000000000..454f140209 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua @@ -0,0 +1,14 @@ +return function(opts, driver, device) + local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { + { model = "E240-KR116Z-HA" } + } + + for _, fingerprint in ipairs(ZIGBEE_METERING_SWITCH_FINGERPRINTS) do + if device:get_model() == fingerprint.model then + local subdriver = require("ezex") + return true, subdriver + end + end + + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua index f88c39bb75..7f338e1b11 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua @@ -15,21 +15,6 @@ local zigbee_constants = require "st.zigbee.constants" local configurations = require "configurations" -local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { - { model = "E240-KR116Z-HA" } -} - -local is_zigbee_ezex_switch = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_METERING_SWITCH_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - local subdriver = require("ezex") - return true, subdriver - end - end - - return false -end - local do_init = function(self, device) device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 1000, {persist = true}) @@ -40,7 +25,7 @@ local ezex_switch_handler = { lifecycle_handlers = { init = configurations.power_reconfig_wrapper(do_init) }, - can_handle = is_zigbee_ezex_switch + can_handle = require("ezex.can_handle"), } return ezex_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua new file mode 100644 index 0000000000..1c23272f30 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Function to determine if the driver can handle this device +return function(opts, driver, device, ...) + local FRIENT_SMART_PLUG_FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FRIENT_SMART_PLUG_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("frient") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua new file mode 100644 index 0000000000..a9c7333473 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua @@ -0,0 +1,14 @@ +return { + { mfr = "frient A/S", model = "SPLZB-131" }, + { mfr = "frient A/S", model = "SPLZB-132" }, + { mfr = "frient A/S", model = "SPLZB-134" }, + { mfr = "frient A/S", model = "SPLZB-137" }, + { mfr = "frient A/S", model = "SPLZB-141" }, + { mfr = "frient A/S", model = "SPLZB-142" }, + { mfr = "frient A/S", model = "SPLZB-144" }, + { mfr = "frient A/S", model = "SPLZB-147" }, + { mfr = "frient A/S", model = "SMRZB-143" }, + { mfr = "frient A/S", model = "SMRZB-153" }, + { mfr = "frient A/S", model = "SMRZB-332" }, + { mfr = "frient A/S", model = "SMRZB-342" }, +} diff --git a/drivers/SmartThings/zigbee-switch/src/frient/init.lua b/drivers/SmartThings/zigbee-switch/src/frient/init.lua index c075fa9320..af85f237a6 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/init.lua @@ -27,20 +27,6 @@ local VOLTAGE_MEASUREMENT_DIVISOR_KEY = "_voltage_measurement_divisor" local CURRENT_MEASUREMENT_MULTIPLIER_KEY = "_current_measurement_multiplier" local CURRENT_MEASUREMENT_DIVISOR_KEY = "_current_measurement_divisor" -local FRIENT_SMART_PLUG_FINGERPRINTS = { - { mfr = "frient A/S", model = "SPLZB-131" }, - { mfr = "frient A/S", model = "SPLZB-132" }, - { mfr = "frient A/S", model = "SPLZB-134" }, - { mfr = "frient A/S", model = "SPLZB-137" }, - { mfr = "frient A/S", model = "SPLZB-141" }, - { mfr = "frient A/S", model = "SPLZB-142" }, - { mfr = "frient A/S", model = "SPLZB-144" }, - { mfr = "frient A/S", model = "SPLZB-147" }, - { mfr = "frient A/S", model = "SMRZB-143" }, - { mfr = "frient A/S", model = "SMRZB-153" }, - { mfr = "frient A/S", model = "SMRZB-332" }, - { mfr = "frient A/S", model = "SMRZB-342" }, -} local POWER_FAILURE_ALARM_CODE = 0x03 @@ -156,17 +142,6 @@ local function do_configure(driver, device) device:refresh() end --- Function to determine if the driver can handle this device -local function can_handle_frient_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(FRIENT_SMART_PLUG_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("frient") - return true, subdriver - end - end - return false -end - -- Main driver definition local frient_smart_plug = { NAME = "frient Smart Plug", @@ -192,7 +167,7 @@ local frient_smart_plug = { doConfigure = do_configure, added = device_added, }, - can_handle = can_handle_frient_smart_plug + can_handle = require("frient.can_handle"), } -return frient_smart_plug \ No newline at end of file +return frient_smart_plug diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua new file mode 100644 index 0000000000..08ce9c471f --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua @@ -0,0 +1,19 @@ +return function(opts, driver, device) + local GE_LINK_BULB_FINGERPRINTS = { + ["GE_Appliances"] = { + ["ZLL Light"] = true, + }, + ["GE"] = { + ["Daylight"] = true, + ["SoftWhite"] = true + } +} + + local can_handle = (GE_LINK_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("ge-link-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua index 3bf3fa8952..51bab5110f 100644 --- a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua @@ -17,26 +17,6 @@ local capabilities = require "st.capabilities" local Level = clusters.Level -local GE_LINK_BULB_FINGERPRINTS = { - ["GE_Appliances"] = { - ["ZLL Light"] = true, - }, - ["GE"] = { - ["Daylight"] = true, - ["SoftWhite"] = true - } -} - -local function can_handle_ge_link_bulb(opts, driver, device) - local can_handle = (GE_LINK_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("ge-link-bulb") - return true, subdriver - else - return false - end -end - local function info_changed(driver, device, event, args) local command local new_dim_onoff_value = tonumber(device.preferences.dimOnOff) @@ -81,7 +61,7 @@ local ge_link_bulb = { [capabilities.switchLevel.commands.setLevel.NAME] = set_level_handler } }, - can_handle = can_handle_ge_link_bulb + can_handle = require("ge-link-bulb.can_handle"), } return ge_link_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua new file mode 100644 index 0000000000..de9c477317 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device, ...) + local FINGERPRINTS = require "hanssem.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("hanssem") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua new file mode 100644 index 0000000000..67d2f40022 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua @@ -0,0 +1,8 @@ +return { + { mfr = "Winners", model = "LSS1-101", children = 0 }, + { mfr = "Winners", model = "LSS1-102", children = 1 }, + { mfr = "Winners", model = "LSS1-103", children = 2 }, + { mfr = "Winners", model = "LSS1-204", children = 3 }, + { mfr = "Winners", model = "LSS1-205", children = 4 }, + { mfr = "Winners", model = "LSS1-206", children = 5 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua index bb845984ca..e1f9939946 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua @@ -15,26 +15,8 @@ local stDevice = require "st.device" local configurations = require "configurations" -local FINGERPRINTS = { - { mfr = "Winners", model = "LSS1-101", children = 0 }, - { mfr = "Winners", model = "LSS1-102", children = 1 }, - { mfr = "Winners", model = "LSS1-103", children = 2 }, - { mfr = "Winners", model = "LSS1-204", children = 3 }, - { mfr = "Winners", model = "LSS1-205", children = 4 }, - { mfr = "Winners", model = "LSS1-206", children = 5 } -} - -local function can_handle_hanssem_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("hanssem") - return true, subdriver - end - end - return false -end - local function get_children_amount(device) + local FINGERPRINTS = require "hanssem.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.children @@ -81,7 +63,7 @@ local HanssemSwitch = { added = device_added, init = configurations.power_reconfig_wrapper(device_init) }, - can_handle = can_handle_hanssem_switch + can_handle = require("hanssem.can_handle"), } -return HanssemSwitch \ No newline at end of file +return HanssemSwitch diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua new file mode 100644 index 0000000000..3c507a2627 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua @@ -0,0 +1,14 @@ + +return function(opts, driver, device) + local IKEA_XY_COLOR_BULB_FINGERPRINTS = { + ["IKEA of Sweden"] = { + ["TRADFRI bulb E27 CWS opal 600lm"] = true, + ["TRADFRI bulb E26 CWS opal 600lm"] = true + } + } + local res = (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if res then + return res, require("ikea-xy-color-bulb") + end + return res +end diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua index b7080ad446..193d032336 100644 --- a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua @@ -27,23 +27,6 @@ local HUESAT_TIMER = "huesat_timer" local TARGET_HUE = "target_hue" local TARGET_SAT = "target_sat" -local IKEA_XY_COLOR_BULB_FINGERPRINTS = { - ["IKEA of Sweden"] = { - ["TRADFRI bulb E27 CWS opal 600lm"] = true, - ["TRADFRI bulb E26 CWS opal 600lm"] = true - } -} - -local function can_handle_ikea_xy_color_bulb(opts, driver, device) - local can_handle = (IKEA_XY_COLOR_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("ikea-xy-color-bulb") - return true, subdriver - else - return false - end -end - local device_init = function(self, device) device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentHue.ID) device:remove_configured_attribute(ColorControl.ID, ColorControl.attributes.CurrentSaturation.ID) @@ -192,7 +175,7 @@ local ikea_xy_color_bulb = { } } }, - can_handle = can_handle_ikea_xy_color_bulb + can_handle = require("ikea-xy-color-bulb.can_handle") } return ikea_xy_color_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index dfaba9e573..8a7a05a9cb 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -29,17 +29,6 @@ else lazy_handler = require end -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - local function component_to_endpoint(device, component_id) local ep_num = component_id:match("switch(%d)") return ep_num and tonumber(ep_num) or device.fingerprinted_endpoint_id @@ -76,6 +65,8 @@ local device_init = function(driver, device) end end +local lazy_load_if_possible = require "lazy_load_subdriver" + local zigbee_switch_driver_template = { supported_capabilities = { capabilities.switch, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua new file mode 100644 index 0000000000..325c2bc973 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua @@ -0,0 +1,14 @@ + +return function(opts, driver, device) + local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM31-SN" }, + { mfr = "Inovelli", model = "VZM32-SN" } + } + for _, fp in ipairs(INOVELLI_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 253dcca86c..dd261cdd8a 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -25,11 +25,6 @@ local OccupancySensing = clusters.OccupancySensing local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" -local INOVELLI_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM31-SN" }, - { mfr = "Inovelli", model = "VZM32-SN" } -} - local PRIVATE_CLUSTER_ID = 0xFC31 local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 local PRIVATE_CMD_NOTIF_ID = 0x01 @@ -112,16 +107,6 @@ local preferences_calculate_parameter = function(new_value, type, number) end end -local can_handle_inovelli = function(opts, driver, device) - for _, fp in ipairs(INOVELLI_FINGERPRINTS) do - if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then - local subdriver = require("inovelli") - return true, subdriver - end - end - return false -end - local function to_boolean(value) if value == 0 or value == "0" then return false @@ -379,9 +364,7 @@ local inovelli = { } } }, - sub_drivers = { - require("inovelli/vzm32-sn"), - }, + sub_drivers = require("inovelli.sub_drivers"), capability_handlers = { [capabilities.switch.ID] = { [capabilities.switch.commands.on.NAME] = on_handler, @@ -400,7 +383,7 @@ local inovelli = { [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, } }, - can_handle = can_handle_inovelli + can_handle = require("inovelli.can_handle"), } -return inovelli \ No newline at end of file +return inovelli diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua new file mode 100644 index 0000000000..c7f0c32bfc --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua @@ -0,0 +1,5 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("inovelli.vzm32-sn") +} diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua new file mode 100644 index 0000000000..e2496028a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua @@ -0,0 +1,13 @@ + +return function(opts, driver, device) + local INOVELLI_VZM32_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM32-SN" }, + } + for _, fp in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local sub_driver = require("inovelli.vzm32-sn") + return true, sub_driver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua index 2be05dad3e..6b46885553 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -20,21 +20,9 @@ local inovelli_common = require "inovelli.common" local OccupancySensing = clusters.OccupancySensing -local INOVELLI_VZM32_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM32-SN" }, -} - local PRIVATE_CLUSTER_ID = 0xFC31 local MFG_CODE = 0x122F -local function can_handle_inovelli_vzm32_sn(opts, driver, device) - for _, fp in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do - if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then - return true - end - end - return false -end local function configure_illuminance_reporting(device) local min_lux_change = 15 @@ -43,7 +31,7 @@ local function configure_illuminance_reporting(device) end local function refresh_handler(driver, device, command) - if device.network_type ~= device.NETWORK_TYPE_CHILD then + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then device:refresh() device:send(OccupancySensing.attributes.Occupancy:read(device)) else @@ -76,7 +64,7 @@ end local vzm32_sn = { NAME = "inovelli vzm32-sn device-specific", - can_handle = can_handle_inovelli_vzm32_sn, + can_handle = require("inovelli.vzm32-sn.can_handle"), lifecycle_handlers = { added = device_added, doConfigure = device_configure, diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua new file mode 100644 index 0000000000..be9bd75694 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua @@ -0,0 +1,15 @@ +return function(opts, driver, device) + local JASCO_SWTICH_FINGERPRINTS = { + { mfr = "Jasco Products", model = "43095" }, + { mfr = "Jasco Products", model = "43132" }, + { mfr = "Jasco Products", model = "43078" } +} + + for _, fingerprint in ipairs(JASCO_SWTICH_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("jasco") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua index 6c90115a22..250903d46a 100644 --- a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua @@ -17,22 +17,6 @@ local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering local constants = require "st.zigbee.constants" -local JASCO_SWTICH_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43095" }, - { mfr = "Jasco Products", model = "43132" }, - { mfr = "Jasco Products", model = "43078" } -} - -local is_jasco_switch = function(opts, driver, device) - for _, fingerprint in ipairs(JASCO_SWTICH_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("jasco") - return true, subdriver - end - end - return false -end - local device_added = function(self, device) local customEnergyDivisor = 10000 device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) @@ -63,7 +47,7 @@ local jasco_switch = { added = device_added, doConfigure = do_configure, }, - can_handle = is_jasco_switch + can_handle = require("jasco.can_handle"), } return jasco_switch diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua new file mode 100644 index 0000000000..991547f9d1 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua @@ -0,0 +1,13 @@ +return function(opts, driver, device, ...) +local FINGERPRINTS = { + { mfr = "LAISIAO", model = "yuba" }, +} + + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("laisiao") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua index edb829bf1f..5a26e01f34 100755 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua @@ -16,19 +16,7 @@ local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local configurations = require "configurations" -local FINGERPRINTS = { - { mfr = "LAISIAO", model = "yuba" }, -} -local function can_handle_laisiao(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("laisiao") - return true, subdriver - end - end - return false -end local function component_to_endpoint(device, component_id) if component_id == "main" then @@ -78,7 +66,7 @@ local laisiao_bath_heater = { [capabilities.switch.commands.on.NAME] = on_handler } }, - can_handle = can_handle_laisiao + can_handle = require("laisiao.can_handle"), } return laisiao_bath_heater diff --git a/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..80dc2101ae --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua @@ -0,0 +1,12 @@ +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua new file mode 100644 index 0000000000..d780d70d06 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local FINGERPRINTS = require "multi-switch-no-master.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model and (device:get_manufacturer() == nil or device:get_manufacturer() == fingerprint.mfr) then + local subdriver = require("multi-switch-no-master") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua new file mode 100644 index 0000000000..f8bb368525 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua @@ -0,0 +1,43 @@ +return { + { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S240R-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S250-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "PM-S340-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "PM-S340R-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "PM-S350-ZB", children = 2 }, + { mfr = "DAWON_DNS", model = "ST-S250-ZB", children = 1 }, + { mfr = "DAWON_DNS", model = "ST-S350-ZB", children = 2 }, + { mfr = "ORVIBO", model = "074b3ffba5a045b7afd94c47079dd553", children = 1 }, + { mfr = "ORVIBO", model = "9f76c9f31b4c4a499e3aca0977ac4494", children = 2 }, + { mfr = "REXENSE", model = "HY0002", children = 1 }, + { mfr = "REXENSE", model = "HY0003", children = 2 }, + { mfr = "REX", model = "HY0096", children = 1 }, + { mfr = "REX", model = "HY0097", children = 2 }, + { mfr = "HEIMAN", model = "HS2SW2L-EFR-3.0", children = 1 }, + { mfr = "HEIMAN", model = "HS2SW3L-EFR-3.0", children = 2 }, + { mfr = "HEIMAN", model = "HS6SW2A-W-EF-3.0", children = 1 }, + { mfr = "HEIMAN", model = "HS6SW3A-W-EF-3.0", children = 2 }, + { mfr = "eWeLink", model = "ZB-SW02", children = 1 }, + { mfr = "eWeLink", model = "ZB-SW03", children = 2 }, + { mfr = "eWeLink", model = "ZB-SW04", children = 3 }, + { mfr = "SMARTvill", model = "SLA01", children = 0 }, + { mfr = "SMARTvill", model = "SLA02", children = 1 }, + { mfr = "SMARTvill", model = "SLA03", children = 2 }, + { mfr = "SMARTvill", model = "SLA04", children = 3 }, + { mfr = "SMARTvill", model = "SLA05", children = 4 }, + { mfr = "SMARTvill", model = "SLA06", children = 5 }, + { mfr = "ShinaSystem", model = "SBM300Z2", children = 1 }, + { mfr = "ShinaSystem", model = "SBM300Z3", children = 2 }, + { mfr = "ShinaSystem", model = "SBM300Z4", children = 3 }, + { mfr = "ShinaSystem", model = "SBM300Z5", children = 4 }, + { mfr = "ShinaSystem", model = "SBM300Z6", children = 5 }, + { mfr = "ShinaSystem", model = "SQM300Z2", children = 1 }, + { mfr = "ShinaSystem", model = "SQM300Z3", children = 2 }, + { mfr = "ShinaSystem", model = "SQM300Z4", children = 3 }, + { mfr = "ShinaSystem", model = "SQM300Z6", children = 5 }, + { model = "E220-KR2N0Z0-HA", children = 1 }, + { model = "E220-KR3N0Z0-HA", children = 2 }, + { model = "E220-KR4N0Z0-HA", children = 3 }, + { model = "E220-KR5N0Z0-HA", children = 4 }, + { model = "E220-KR6N0Z0-HA", children = 5 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua index bec5b1d87d..85bfb6ae96 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua @@ -15,62 +15,8 @@ local st_device = require "st.device" local utils = require "st.utils" local configurations = require "configurations" -local MULTI_SWITCH_NO_MASTER_FINGERPRINTS = { - { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S240R-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S250-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "PM-S340-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "PM-S340R-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "PM-S350-ZB", children = 2 }, - { mfr = "DAWON_DNS", model = "ST-S250-ZB", children = 1 }, - { mfr = "DAWON_DNS", model = "ST-S350-ZB", children = 2 }, - { mfr = "ORVIBO", model = "074b3ffba5a045b7afd94c47079dd553", children = 1 }, - { mfr = "ORVIBO", model = "9f76c9f31b4c4a499e3aca0977ac4494", children = 2 }, - { mfr = "REXENSE", model = "HY0002", children = 1 }, - { mfr = "REXENSE", model = "HY0003", children = 2 }, - { mfr = "REX", model = "HY0096", children = 1 }, - { mfr = "REX", model = "HY0097", children = 2 }, - { mfr = "HEIMAN", model = "HS2SW2L-EFR-3.0", children = 1 }, - { mfr = "HEIMAN", model = "HS2SW3L-EFR-3.0", children = 2 }, - { mfr = "HEIMAN", model = "HS6SW2A-W-EF-3.0", children = 1 }, - { mfr = "HEIMAN", model = "HS6SW3A-W-EF-3.0", children = 2 }, - { mfr = "eWeLink", model = "ZB-SW02", children = 1 }, - { mfr = "eWeLink", model = "ZB-SW03", children = 2 }, - { mfr = "eWeLink", model = "ZB-SW04", children = 3 }, - { mfr = "SMARTvill", model = "SLA01", children = 0 }, - { mfr = "SMARTvill", model = "SLA02", children = 1 }, - { mfr = "SMARTvill", model = "SLA03", children = 2 }, - { mfr = "SMARTvill", model = "SLA04", children = 3 }, - { mfr = "SMARTvill", model = "SLA05", children = 4 }, - { mfr = "SMARTvill", model = "SLA06", children = 5 }, - { mfr = "ShinaSystem", model = "SBM300Z2", children = 1 }, - { mfr = "ShinaSystem", model = "SBM300Z3", children = 2 }, - { mfr = "ShinaSystem", model = "SBM300Z4", children = 3 }, - { mfr = "ShinaSystem", model = "SBM300Z5", children = 4 }, - { mfr = "ShinaSystem", model = "SBM300Z6", children = 5 }, - { mfr = "ShinaSystem", model = "SQM300Z2", children = 1 }, - { mfr = "ShinaSystem", model = "SQM300Z3", children = 2 }, - { mfr = "ShinaSystem", model = "SQM300Z4", children = 3 }, - { mfr = "ShinaSystem", model = "SQM300Z6", children = 5 }, - { model = "E220-KR2N0Z0-HA", children = 1 }, - { model = "E220-KR3N0Z0-HA", children = 2 }, - { model = "E220-KR4N0Z0-HA", children = 3 }, - { model = "E220-KR5N0Z0-HA", children = 4 }, - { model = "E220-KR6N0Z0-HA", children = 5 } -} - -local function is_multi_switch_no_master(opts, driver, device) - for _, fingerprint in ipairs(MULTI_SWITCH_NO_MASTER_FINGERPRINTS) do - if device:get_model() == fingerprint.model and (device:get_manufacturer() == nil or device:get_manufacturer() == fingerprint.mfr) then - local subdriver = require("multi-switch-no-master") - return true, subdriver - end - end - return false -end - local function get_children_amount(device) - for _, fingerprint in ipairs(MULTI_SWITCH_NO_MASTER_FINGERPRINTS) do + for _, fingerprint in ipairs(require("multi-switch-no-master.fingerprints")) do if device:get_model() == fingerprint.model then return fingerprint.children end @@ -117,8 +63,7 @@ local multi_switch_no_master = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = is_multi_switch_no_master + can_handle = require("multi-switch-no-master.can_handle"), } return multi_switch_no_master - diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua new file mode 100644 index 0000000000..ce1a124104 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua @@ -0,0 +1,8 @@ +return function(opts, driver, device) + local st_device = require "st.device" + + if device.network_type ~= st_device.NETWORK_TYPE_ZIGBEE and device.network_type ~= st_device.NETWORK_TYPE_CHILD then + return true, require("non_zigbee_devices") + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua index bc6caa8580..628eb825ff 100644 --- a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua @@ -17,15 +17,8 @@ -- This patch works around it until hubcore 0.59 is released with -- https://smartthings.atlassian.net/browse/CHAD-16552 -local st_device = require "st.device" local log = require "log" -local function can_handle(opts, driver, device) - if device.network_type ~= st_device.NETWORK_TYPE_ZIGBEE and device.network_type ~= st_device.NETWORK_TYPE_CHILD then - return true, require("non_zigbee_devices") - end - return false -end local function device_added(driver, device, event) log.info(string.format("Non zigbee device added: %s", device)) @@ -51,7 +44,7 @@ local non_zigbee_devices = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = can_handle + can_handle = require("non_zigbee_devices.can_handle"), } -return non_zigbee_devices \ No newline at end of file +return non_zigbee_devices diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua new file mode 100644 index 0000000000..7fee03f115 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua @@ -0,0 +1,13 @@ +return function(opts, driver, device) +local ZIGBEE_METERING_PLUG_FINGERPRINTS = { + { mfr = "REXENSE", model = "HY0105" } -- HONYAR Outlet" +} + for _, fingerprint in ipairs(ZIGBEE_METERING_PLUG_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("rexense") + return true, subdriver + end + end + + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua index d0d457928e..598afa75ec 100644 --- a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua @@ -17,10 +17,6 @@ local capabilities = require "st.capabilities" local OnOff = zcl_clusters.OnOff -local ZIGBEE_METERING_PLUG_FINGERPRINTS = { - { mfr = "REXENSE", model = "HY0105" } -- HONYAR Outlet" -} - local function switch_on_handler(driver, device, command) device:send_to_component(command.component, OnOff.server.commands.On(device)) device:send(OnOff.server.commands.On(device):to_endpoint(0x02)) @@ -31,16 +27,6 @@ local function switch_off_handler(driver, device, command) device:send(OnOff.server.commands.Off(device):to_endpoint(0x02)) end -local function is_zigbee_metering_plug(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_METERING_PLUG_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("rexense") - return true, subdriver - end - end - - return false -end local zigbee_metering_plug = { NAME = "zigbee metering plug", @@ -50,7 +36,7 @@ local zigbee_metering_plug = { [capabilities.switch.commands.off.NAME] = switch_off_handler } }, - can_handle = is_zigbee_metering_plug + can_handle = require("rexense.can_handle"), } return zigbee_metering_plug diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua new file mode 100644 index 0000000000..6d255174d6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua @@ -0,0 +1,20 @@ +return function(opts, driver, device) +local RGB_BULB_FINGERPRINTS = { + ["OSRAM"] = { + ["Gardenspot RGB"] = true, + ["LIGHTIFY Gardenspot RGB"] = true + }, + ["LEDVANCE"] = { + ["Outdoor Accent RGB"] = true + } +} + + + local can_handle = (RGB_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("rgb-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua index ce0f9976db..03ed2b1574 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua @@ -19,25 +19,6 @@ local OnOff = clusters.OnOff local Level = clusters.Level local ColorControl = clusters.ColorControl -local RGB_BULB_FINGERPRINTS = { - ["OSRAM"] = { - ["Gardenspot RGB"] = true, - ["LIGHTIFY Gardenspot RGB"] = true - }, - ["LEDVANCE"] = { - ["Outdoor Accent RGB"] = true - } -} - -local function can_handle_rgb_bulb(opts, driver, device) - local can_handle = (RGB_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("rgb-bulb") - return true, subdriver - else - return false - end -end local function do_refresh(driver, device) local attributes = { @@ -66,7 +47,7 @@ local rgb_bulb = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_rgb_bulb + can_handle = require("rgb-bulb.can_handle"), } return rgb_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua new file mode 100644 index 0000000000..77c16d7b90 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local RGBW_BULB_FINGERPRINTS = require "rgbw-bulb.fingerprints" + local can_handle = (RGBW_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("rgbw-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua new file mode 100644 index 0000000000..e0c8c3dd93 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua @@ -0,0 +1,71 @@ +return { + ["Samsung Electronics"] = { + ["SAMSUNG-ITM-Z-002"] = true + }, + ["Juno"] = { + ["ABL-LIGHT-Z-201"] = true + }, + ["AduroSmart Eria"] = { + ["AD-RGBW3001"] = true + }, + ["Aurora"] = { + ["RGBCXStrip50AU"] = true, + ["RGBGU10Bulb50AU"] = true, + ["RGBBulb51AU"] = true + }, + ["CWD"] = { + ["ZB.A806Ergbw-A001"] = true, + ["ZB.A806Brgbw-A001"] = true, + ["ZB.M350rgbw-A001"] = true + }, + ["innr"] = { + ["RB 285 C"] = true, + ["BY 285 C"] = true, + ["RB 250 C"] = true, + ["RS 230 C"] = true, + ["AE 280 C"] = true + }, + ["MLI"] = { + ["ZBT-ExtendedColor"] = true + }, + ["OSRAM"] = { + ["LIGHTIFY Flex RGBW"] = true, + ["Flex RGBW"] = true, + ["LIGHTIFY A19 RGBW"] = true, + ["LIGHTIFY BR RGBW"] = true, + ["LIGHTIFY RT RGBW"] = true, + ["LIGHTIFY FLEX OUTDOOR RGBW"] = true + }, + ["LEDVANCE"] = { + ["RT HO RGBW"] = true, + ["A19 RGBW"] = true, + ["FLEX Outdoor RGBW"] = true, + ["FLEX RGBW"] = true, + ["BR30 RGBW"] = true, + ["RT RGBW"] = true, + ["Outdoor Pathway RGBW"] = true, + ["Flex RGBW Pro"] = true + }, + ["LEEDARSON LIGHTING"] = { + ["5ZB-A806ST-Q1G"] = true + }, + ["sengled"] = { + ["E11-N1EA"] = true, + ["E12-N1E"] = true, + ["E21-N1EA"] = true, + ["E1G-G8E"] = true, + ["E11-U3E"] = true, + ["E11-U2E"] = true, + ["E1F-N5E"] = true, + ["E23-N13"] = true + }, + ["Neuhaus Lighting Group"] = { + ["ZBT-ExtendedColor"] = true + }, + ["Ajaxonline"] = { + ["AJ-RGBCCT 5 in 1"] = true + }, + ["Ajax online Ltd"] = { + ["AJ_ZB30_GU10"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua index fb3a1a32f2..aa9b25e9ae 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua @@ -20,88 +20,6 @@ local OnOff = clusters.OnOff local Level = clusters.Level local ColorControl = clusters.ColorControl -local RGBW_BULB_FINGERPRINTS = { - ["Samsung Electronics"] = { - ["SAMSUNG-ITM-Z-002"] = true - }, - ["Juno"] = { - ["ABL-LIGHT-Z-201"] = true - }, - ["AduroSmart Eria"] = { - ["AD-RGBW3001"] = true - }, - ["Aurora"] = { - ["RGBCXStrip50AU"] = true, - ["RGBGU10Bulb50AU"] = true, - ["RGBBulb51AU"] = true - }, - ["CWD"] = { - ["ZB.A806Ergbw-A001"] = true, - ["ZB.A806Brgbw-A001"] = true, - ["ZB.M350rgbw-A001"] = true - }, - ["innr"] = { - ["RB 285 C"] = true, - ["BY 285 C"] = true, - ["RB 250 C"] = true, - ["RS 230 C"] = true, - ["AE 280 C"] = true - }, - ["MLI"] = { - ["ZBT-ExtendedColor"] = true - }, - ["OSRAM"] = { - ["LIGHTIFY Flex RGBW"] = true, - ["Flex RGBW"] = true, - ["LIGHTIFY A19 RGBW"] = true, - ["LIGHTIFY BR RGBW"] = true, - ["LIGHTIFY RT RGBW"] = true, - ["LIGHTIFY FLEX OUTDOOR RGBW"] = true - }, - ["LEDVANCE"] = { - ["RT HO RGBW"] = true, - ["A19 RGBW"] = true, - ["FLEX Outdoor RGBW"] = true, - ["FLEX RGBW"] = true, - ["BR30 RGBW"] = true, - ["RT RGBW"] = true, - ["Outdoor Pathway RGBW"] = true, - ["Flex RGBW Pro"] = true - }, - ["LEEDARSON LIGHTING"] = { - ["5ZB-A806ST-Q1G"] = true - }, - ["sengled"] = { - ["E11-N1EA"] = true, - ["E12-N1E"] = true, - ["E21-N1EA"] = true, - ["E1G-G8E"] = true, - ["E11-U3E"] = true, - ["E11-U2E"] = true, - ["E1F-N5E"] = true, - ["E23-N13"] = true - }, - ["Neuhaus Lighting Group"] = { - ["ZBT-ExtendedColor"] = true - }, - ["Ajaxonline"] = { - ["AJ-RGBCCT 5 in 1"] = true - }, - ["Ajax online Ltd"] = { - ["AJ_ZB30_GU10"] = true - } -} - -local function can_handle_rgbw_bulb(opts, driver, device) - local can_handle = (RGBW_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("rgbw-bulb") - return true, subdriver - else - return false - end -end - local function do_refresh(driver, device) local attributes = { OnOff.attributes.OnOff, @@ -149,7 +67,7 @@ local rgbw_bulb = { doConfigure = do_configure, added = do_added }, - can_handle = can_handle_rgbw_bulb + can_handle = require("rgbw-bulb.can_handle"), } return rgbw_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua new file mode 100644 index 0000000000..4544c17d64 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua @@ -0,0 +1,14 @@ +return function(opts, driver, device) +local ROBB_DIMMER_FINGERPRINTS = { + { mfr = "ROBB smarrt", model = "ROB_200-011-0" }, + { mfr = "ROBB smarrt", model = "ROB_200-014-0" } +} + for _, fingerprint in ipairs(ROBB_DIMMER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("robb") + return true, subdriver + end + end + return false +end + diff --git a/drivers/SmartThings/zigbee-switch/src/robb/init.lua b/drivers/SmartThings/zigbee-switch/src/robb/init.lua index 4593b4339f..9081d058db 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/init.lua @@ -18,27 +18,12 @@ local capabilities = require "st.capabilities" local configurations = require "configurations" local SimpleMetering = zcl_clusters.SimpleMetering -local ROBB_DIMMER_FINGERPRINTS = { - { mfr = "ROBB smarrt", model = "ROB_200-011-0" }, - { mfr = "ROBB smarrt", model = "ROB_200-014-0" } -} - -local function is_robb_dimmer(opts, driver, device) - for _, fingerprint in ipairs(ROBB_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("robb") - return true, subdriver - end - end - return false -end local do_init = function(driver, device) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000000, {persist = true}) device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) end - local function power_meter_handler(driver, device, value, zb_rx) local raw_value = value.value local multiplier = device:get_field(constants.ELECTRICAL_MEASUREMENT_MULTIPLIER_KEY) or 1 @@ -60,7 +45,7 @@ local robb_dimmer_handler = { lifecycle_handlers = { init = configurations.power_reconfig_wrapper(do_init) }, - can_handle = is_robb_dimmer + can_handle = require("robb.can_handle"), } return robb_dimmer_handler diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua new file mode 100644 index 0000000000..326de13801 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua @@ -0,0 +1,9 @@ +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "DM2500ZB" + if can_handle then + local subdriver = require("sinope-dimmer") + return true, subdriver + else + return false + end + end diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua index 981df6ff5e..c2aba06bda 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua @@ -104,15 +104,7 @@ local zigbee_sinope_dimmer = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "DM2500ZB" - if can_handle then - local subdriver = require("sinope-dimmer") - return true, subdriver - else - return false - end - end + can_handle = require("sinope-dimmer.can_handle"), } return zigbee_sinope_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua new file mode 100644 index 0000000000..90b7965184 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua @@ -0,0 +1,9 @@ +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "SW2500ZB" + if can_handle then + local subdriver = require("sinope") + return true, subdriver + else + return false + end + end diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua index d3e34513a5..a7101bb971 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua @@ -41,15 +41,7 @@ local zigbee_sinope_switch = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "SW2500ZB" - if can_handle then - local subdriver = require("sinope") - return true, subdriver - else - return false - end - end + can_handle = require("sinope.can_handle"), } return zigbee_sinope_switch diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua new file mode 100644 index 0000000000..f471c99151 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua @@ -0,0 +1,18 @@ +local function is_multi_endpoint(device) + local main_endpoint = device:get_endpoint(0x0006) + for _, ep in ipairs(device.zigbee_endpoints) do + if ep.id ~= main_endpoint then + return true + end + end + return false +end + +return function(opts, driver, device) + local TUYA_MFR_HEADER = "_TZ" + if string.sub(device:get_manufacturer(),1,3) == TUYA_MFR_HEADER and is_multi_endpoint(device) then -- if it is a tuya device, then send the magic packet + local subdriver = require("tuya-multi") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua index c4857e9085..b0c7b85202 100644 --- a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua @@ -6,26 +6,6 @@ local messages = require "st.zigbee.messages" local zb_const = require "st.zigbee.constants" local read_attribute = require "st.zigbee.zcl.global_commands.read_attribute" -local TUYA_MFR_HEADER = "_TZ" - -local function is_multi_endpoint(device) - local main_endpoint = device:get_endpoint(clusters.OnOff.ID) - for _, ep in ipairs(device.zigbee_endpoints) do - if ep.id ~= main_endpoint then - return true - end - end - return false -end - -local function is_tuya_products(opts, driver, device) - if string.sub(device:get_manufacturer(),1,3) == TUYA_MFR_HEADER and is_multi_endpoint(device) then -- if it is a tuya device, then send the magic packet - local subdriver = require("tuya-multi") - return true, subdriver - end - return false -end - local function read_attribute_function(device, cluster_id, attr_id) local read_body = read_attribute.ReadAttribute( attr_id ) local zclh = zcl_messages.ZclHeader({ @@ -65,7 +45,7 @@ local tuya_switch_handler = { supported_capabilities = { capabilities.switch }, - can_handle = is_tuya_products + can_handle = require("tuya-multi.can_handle"), } -return tuya_switch_handler \ No newline at end of file +return tuya_switch_handler diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua new file mode 100644 index 0000000000..d63ed62a49 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua @@ -0,0 +1,11 @@ +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-switch/src/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua new file mode 100644 index 0000000000..c352b6feb4 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua @@ -0,0 +1,8 @@ +return { + { mfr = "WALL HERO", model = "ACL-401S4I", switches = 4, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S8I", switches = 4, buttons = 4 }, + { mfr = "WALL HERO", model = "ACL-401S3I", switches = 3, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S2I", switches = 2, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401S1I", switches = 1, buttons = 0 }, + { mfr = "WALL HERO", model = "ACL-401ON", switches = 1, buttons = 0 } +} diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua index 79f3110e41..531a7c4bf2 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua @@ -25,26 +25,8 @@ local PRIVATE_CLUSTER_ID = 0x0006 local PRIVATE_ATTRIBUTE_ID = 0x6000 local MFG_CODE = 0x1235 -local FINGERPRINTS = { - { mfr = "WALL HERO", model = "ACL-401S4I", switches = 4, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S8I", switches = 4, buttons = 4 }, - { mfr = "WALL HERO", model = "ACL-401S3I", switches = 3, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S2I", switches = 2, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401S1I", switches = 1, buttons = 0 }, - { mfr = "WALL HERO", model = "ACL-401ON", switches = 1, buttons = 0 } -} - -local function can_handle_wallhero_switch(opts, driver, device, ...) - 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 - local function get_children_info(device) + local FINGERPRINTS = require "wallhero.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.switches, fingerprint.buttons @@ -141,7 +123,7 @@ local wallheroswitch = { } } }, - can_handle = can_handle_wallhero_switch + can_handle = require("wallhero.can_handle"), } return wallheroswitch diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua new file mode 100644 index 0000000000..1b609d16ad --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local WHITE_COLOR_TEMP_BULB_FINGERPRINTS = require "white-color-temp-bulb.fingerprints" + local can_handle = (WHITE_COLOR_TEMP_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("white-color-temp-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua new file mode 100644 index 0000000000..2b4c439e64 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua @@ -0,0 +1,15 @@ +return function(opts, driver, device) + local DURAGREEN_BULB_FINGERPRINTS = { + ["DURAGREEN"] = { + ["DG-CW-02"] = true, + ["DG-CW-01"] = true, + ["DG-CCT-01"] = true + }, + } + local res = (DURAGREEN_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false + if res then + return res, require("white-color-temp-bulb.duragreen") + else + return res + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua index 7df79be32e..4c6d134e0e 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua @@ -17,18 +17,6 @@ local clusters = require "st.zigbee.zcl.clusters" local Level = clusters.Level -local DURAGREEN_BULB_FINGERPRINTS = { - ["DURAGREEN"] = { - ["DG-CW-02"] = true, - ["DG-CW-01"] = true, - ["DG-CCT-01"] = true - }, -} - -local function can_handle_duragreen_bulb(opts, driver, device) - return (DURAGREEN_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] or false -end - local function handle_set_level(driver, device, cmd) local level = math.floor(cmd.args.level/100.0 * 254) local transtition_time = cmd.args.rate or 0xFFFF @@ -46,7 +34,7 @@ local duragreen_color_temp_bulb = { [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level } }, - can_handle = can_handle_duragreen_bulb + can_handle = require("white-color-temp-bulb.duragreen.can_handle") } return duragreen_color_temp_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua new file mode 100644 index 0000000000..6824bf40ae --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua @@ -0,0 +1,96 @@ +return { + ["DURAGREEN"] = { + ["DG-CW-02"] = true, + ["DG-CW-01"] = true, + ["DG-CCT-01"] = true + }, + ["Samsung Electronics"] = { + ["ABL-LIGHT-Z-001"] = true, + ["SAMSUNG-ITM-Z-001"] = true + }, + ["Juno"] = { + ["ABL-LIGHT-Z-001"] = true + }, + ["AduroSmart Eria"] = { + ["AD-ColorTemperature3001"] = true + }, + ["Aurora"] = { + ["TWBulb51AU"] = true, + ["TWMPROZXBulb50AU"] = true, + ["TWStrip50AU"] = true, + ["TWGU10Bulb50AU"] = true, + ["TWCLBulb50AU"] = true + }, + ["CWD"] = { + ["ZB.A806Ecct-A001"] = true, + ["ZB.A806Bcct-A001"] = true, + ["ZB.M350cct-A001"] = true + }, + ["ETI"] = { + ["Zigbee CCT Downlight"] = true + }, + ["The Home Depot"] = { + ["Ecosmart-ZBT-BR30-CCT-Bulb"] = true, + ["Ecosmart-ZBT-A19-CCT-Bulb"] = true + }, + ["IKEA of Sweden"] = { + ["GUNNARP panel round"] = true, + ["LEPTITER Recessed spot light"] = true, + ["TRADFRI bulb E12 WS opal 600lm"] = true, + ["TRADFRI bulb E14 WS 470lm"] = true, + ["TRADFRI bulb E14 WS opal 600lm"] = true, + ["TRADFRI bulb E26 WS clear 806lm"] = true, + ["TRADFRI bulb E27 WS clear 806lm"] = true, + ["TRADFRI bulb E26 WS opal 1000lm"] = true, + ["TRADFRI bulb E27 WS opal 1000lm"] = true + }, + ["Megaman"] = { + ["Z3-ColorTemperature"] = true + }, + ["innr"] = { + ["RB 248 T"] = true, + ["RB 278 T"] = true, + ["RS 228 T"] = true + }, + ["OSRAM"] = { + ["LIGHTIFY BR Tunable White"] = true, + ["LIGHTIFY RT Tunable White"] = true, + ["Classic A60 TW"] = true, + ["LIGHTIFY A19 Tunable White"] = true, + ["Classic B40 TW - LIGHTIFY"] = true, + ["LIGHTIFY Conv Under Cabinet TW"] = true, + ["ColorstripRGBW"] = true, + ["LIGHTIFY Edge-lit Flushmount TW"] = true, + ["LIGHTIFY Surface TW"] = true, + ["LIGHTIFY Under Cabinet TW"] = true, + ["LIGHTIFY Edge-lit flushmount"] = true + }, + ["LEDVANCE"] = { + ["A19 TW 10 year"] = true, + ["MR16 TW"] = true, + ["BR30 TW"] = true, + ["RT TW"] = true + }, + ["Smarthome"] = { + ["S111-202A"] = true + }, + ["lk"] = { + ["ZBT-CCTLight-GLS0108"] = true + }, + ["MLI"] = { + ["ZBT-ColorTemperature"] = true + }, + ["sengled"] = { + ["Z01-A19NAE26"] = true, + ["Z01-A191AE26W"] = true, + ["Z01-A60EAB22"] = true, + ["Z01-A60EAE27"] = true + }, + ["Third Reality, Inc"] = { + ["3RSL011Z"] = true, + ["3RSL012Z"] = true + }, + ["Ajax Online"] = { + ["CCT"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua index 70efb29afa..40b482604c 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua @@ -18,113 +18,6 @@ local colorTemperature_defaults = require "st.zigbee.defaults.colorTemperature_d local ColorControl = clusters.ColorControl -local WHITE_COLOR_TEMP_BULB_FINGERPRINTS = { - ["DURAGREEN"] = { - ["DG-CW-02"] = true, - ["DG-CW-01"] = true, - ["DG-CCT-01"] = true - }, - ["Samsung Electronics"] = { - ["ABL-LIGHT-Z-001"] = true, - ["SAMSUNG-ITM-Z-001"] = true - }, - ["Juno"] = { - ["ABL-LIGHT-Z-001"] = true - }, - ["AduroSmart Eria"] = { - ["AD-ColorTemperature3001"] = true - }, - ["Aurora"] = { - ["TWBulb51AU"] = true, - ["TWMPROZXBulb50AU"] = true, - ["TWStrip50AU"] = true, - ["TWGU10Bulb50AU"] = true, - ["TWCLBulb50AU"] = true - }, - ["CWD"] = { - ["ZB.A806Ecct-A001"] = true, - ["ZB.A806Bcct-A001"] = true, - ["ZB.M350cct-A001"] = true - }, - ["ETI"] = { - ["Zigbee CCT Downlight"] = true - }, - ["The Home Depot"] = { - ["Ecosmart-ZBT-BR30-CCT-Bulb"] = true, - ["Ecosmart-ZBT-A19-CCT-Bulb"] = true - }, - ["IKEA of Sweden"] = { - ["GUNNARP panel round"] = true, - ["LEPTITER Recessed spot light"] = true, - ["TRADFRI bulb E12 WS opal 600lm"] = true, - ["TRADFRI bulb E14 WS 470lm"] = true, - ["TRADFRI bulb E14 WS opal 600lm"] = true, - ["TRADFRI bulb E26 WS clear 806lm"] = true, - ["TRADFRI bulb E27 WS clear 806lm"] = true, - ["TRADFRI bulb E26 WS opal 1000lm"] = true, - ["TRADFRI bulb E27 WS opal 1000lm"] = true - }, - ["Megaman"] = { - ["Z3-ColorTemperature"] = true - }, - ["innr"] = { - ["RB 248 T"] = true, - ["RB 278 T"] = true, - ["RS 228 T"] = true - }, - ["OSRAM"] = { - ["LIGHTIFY BR Tunable White"] = true, - ["LIGHTIFY RT Tunable White"] = true, - ["Classic A60 TW"] = true, - ["LIGHTIFY A19 Tunable White"] = true, - ["Classic B40 TW - LIGHTIFY"] = true, - ["LIGHTIFY Conv Under Cabinet TW"] = true, - ["ColorstripRGBW"] = true, - ["LIGHTIFY Edge-lit Flushmount TW"] = true, - ["LIGHTIFY Surface TW"] = true, - ["LIGHTIFY Under Cabinet TW"] = true, - ["LIGHTIFY Edge-lit flushmount"] = true - }, - ["LEDVANCE"] = { - ["A19 TW 10 year"] = true, - ["MR16 TW"] = true, - ["BR30 TW"] = true, - ["RT TW"] = true - }, - ["Smarthome"] = { - ["S111-202A"] = true - }, - ["lk"] = { - ["ZBT-CCTLight-GLS0108"] = true - }, - ["MLI"] = { - ["ZBT-ColorTemperature"] = true - }, - ["sengled"] = { - ["Z01-A19NAE26"] = true, - ["Z01-A191AE26W"] = true, - ["Z01-A60EAB22"] = true, - ["Z01-A60EAE27"] = true - }, - ["Third Reality, Inc"] = { - ["3RSL011Z"] = true, - ["3RSL012Z"] = true - }, - ["Ajax Online"] = { - ["CCT"] = true - } -} - -local function can_handle_white_color_temp_bulb(opts, driver, device) - local can_handle = (WHITE_COLOR_TEMP_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("white-color-temp-bulb") - return true, subdriver - else - return false - end -end - local function set_color_temperature_handler(driver, device, cmd) colorTemperature_defaults.set_color_temperature(driver, device, cmd) @@ -140,10 +33,8 @@ local white_color_temp_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature_handler } }, - sub_drivers = { - require("white-color-temp-bulb.duragreen"), - }, - can_handle = can_handle_white_color_temp_bulb + sub_drivers = require("white-color-temp-bulb.sub_drivers"), + can_handle = require("white-color-temp-bulb.can_handle"), } return white_color_temp_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua new file mode 100644 index 0000000000..450c97b97b --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua @@ -0,0 +1,4 @@ +local lazy_load = require "lazy_load_subdriver" +return { + lazy_load("white-color-temp-bulb.duragreen"), +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua new file mode 100644 index 0000000000..7776397269 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua @@ -0,0 +1,13 @@ +local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { + { mfr = "Jasco Products", model = "43082" } +} + +return function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dimmer-power-energy") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua index 75f2af82c1..b3c1a61969 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua @@ -18,20 +18,6 @@ local SimpleMetering = clusters.SimpleMetering local constants = require "st.zigbee.constants" local configurations = require "configurations" -local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { - { mfr = "Jasco Products", model = "43082" } -} - -local is_zigbee_dimmer_power_energy = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dimmer-power-energy") - return true, subdriver - end - end - return false -end - local device_init = function(self, device) local customEnergyDivisor = 10000 device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, customEnergyDivisor, {persist = true}) @@ -69,7 +55,7 @@ local zigbee_dimmer_power_energy_handler = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_zigbee_dimmer_power_energy + can_handle = require("zigbee-dimmer-power-energy.can_handle"), } return zigbee_dimmer_power_energy_handler diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua new file mode 100644 index 0000000000..1c2f6d3f69 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local DIMMING_LIGHT_FINGERPRINTS = require "zigbee-dimming-light.fingerprints" + for _, fingerprint in ipairs(DIMMING_LIGHT_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dimming-light") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua new file mode 100644 index 0000000000..b90edb17e8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua @@ -0,0 +1,33 @@ +return { + {mfr = "Vimar", model = "DimmerSwitch_v1.0"}, -- Vimar Smart Dimmer Switch + {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM"}, -- SYLVANIA Smart A19 Soft White + {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM 10 Year"}, -- SYLVANIA Smart 10-Year A19 + {mfr = "OSRAM SYLVANIA", model = "iQBR30"}, -- SYLVANIA Ultra iQ + {mfr = "OSRAM", model = "LIGHTIFY PAR38 ON/OFF/DIM"}, -- SYLVANIA Smart PAR38 Soft White + {mfr = "OSRAM", model = "LIGHTIFY BR ON/OFF/DIM"}, -- SYLVANIA Smart BR30 Soft White + {mfr = "sengled", model = "E11-G13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G23"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-G33"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E12-N15"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-N13"}, -- Sengled Element Classic + {mfr = "sengled", model = "E11-N14"}, -- Sengled Element Classic + {mfr = "sengled", model = "E1A-AC2"}, -- Sengled DownLight + {mfr = "sengled", model = "E11-N13A"}, -- Sengled Extra Bright Soft White + {mfr = "sengled", model = "E11-N14A"}, -- Sengled Extra Bright Daylight + {mfr = "sengled", model = "E21-N13A"}, -- Sengled Soft White + {mfr = "sengled", model = "E21-N14A"}, -- Sengled Daylight + {mfr = "sengled", model = "E11-U21U31"}, -- Sengled Element Touch + {mfr = "sengled", model = "E13-A21"}, -- Sengled LED Flood Light + {mfr = "sengled", model = "E11-N1G"}, -- Sengled Smart LED Vintage Edison Bulb + {mfr = "sengled", model = "E23-N11"}, -- Sengled Element Classic par38 + {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch + {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer + {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch + {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch + {mfr = "MRVL", model = "MZ100"}, + {mfr = "CREE", model = "Connected A-19 60W Equivalent"}, + {mfr = "Insta GmbH", model = "NEXENTRO Dimming Actuator"} +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 68e924a8f1..9b05f3092f 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -20,40 +20,6 @@ local switch_utils = require "switch_utils" local OnOff = clusters.OnOff local Level = clusters.Level -local DIMMING_LIGHT_FINGERPRINTS = { - {mfr = "Vimar", model = "DimmerSwitch_v1.0"}, -- Vimar Smart Dimmer Switch - {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM"}, -- SYLVANIA Smart A19 Soft White - {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM 10 Year"}, -- SYLVANIA Smart 10-Year A19 - {mfr = "OSRAM SYLVANIA", model = "iQBR30"}, -- SYLVANIA Ultra iQ - {mfr = "OSRAM", model = "LIGHTIFY PAR38 ON/OFF/DIM"}, -- SYLVANIA Smart PAR38 Soft White - {mfr = "OSRAM", model = "LIGHTIFY BR ON/OFF/DIM"}, -- SYLVANIA Smart BR30 Soft White - {mfr = "sengled", model = "E11-G13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G23"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-G33"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E12-N15"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-N13"}, -- Sengled Element Classic - {mfr = "sengled", model = "E11-N14"}, -- Sengled Element Classic - {mfr = "sengled", model = "E1A-AC2"}, -- Sengled DownLight - {mfr = "sengled", model = "E11-N13A"}, -- Sengled Extra Bright Soft White - {mfr = "sengled", model = "E11-N14A"}, -- Sengled Extra Bright Daylight - {mfr = "sengled", model = "E21-N13A"}, -- Sengled Soft White - {mfr = "sengled", model = "E21-N14A"}, -- Sengled Daylight - {mfr = "sengled", model = "E11-U21U31"}, -- Sengled Element Touch - {mfr = "sengled", model = "E13-A21"}, -- Sengled LED Flood Light - {mfr = "sengled", model = "E11-N1G"}, -- Sengled Smart LED Vintage Edison Bulb - {mfr = "sengled", model = "E23-N11"}, -- Sengled Element Classic par38 - {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch - {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer - {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch - {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch - {mfr = "MRVL", model = "MZ100"}, - {mfr = "CREE", model = "Connected A-19 60W Equivalent"}, - {mfr = "Insta GmbH", model = "NEXENTRO Dimming Actuator"} -} - local DIMMING_LIGHT_CONFIGURATION = { { cluster = OnOff.ID, @@ -75,16 +41,6 @@ local DIMMING_LIGHT_CONFIGURATION = { } } -local function can_handle_zigbee_dimming_light(opts, driver, device) - for _, fingerprint in ipairs(DIMMING_LIGHT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dimming-light") - return true, subdriver - end - end - return false -end - local function do_configure(driver, device) device:refresh() device:configure() @@ -107,11 +63,8 @@ local zigbee_dimming_light = { added = device_added, doConfigure = do_configure }, - sub_drivers = { - require("zigbee-dimming-light/osram-iqbr30"), - require("zigbee-dimming-light/zll-dimmer") - }, - can_handle = can_handle_zigbee_dimming_light + sub_drivers = require("zigbee-dimming-light.sub_drivers"), + can_handle = require("zigbee-dimming-light.can_handle"), } return zigbee_dimming_light diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua new file mode 100644 index 0000000000..a5cf2faff0 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua @@ -0,0 +1,7 @@ +return function(opts, driver, device, ...) + local res = device:get_manufacturer() == "OSRAM SYLVANIA" and device:get_model() == "iQBR30" + if res then + return res, require("zigbee-dimming-light.osram-iqbr30") + end + return res +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua index c6d31b7e10..43d22558b8 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua @@ -20,10 +20,6 @@ local Level = clusters.Level local SwitchLevel = capabilities.switchLevel -local function can_handle_osram_iqbr30(opts, driver, device, ...) - return device:get_manufacturer() == "OSRAM SYLVANIA" and device:get_model() == "iQBR30" -end - local function set_switch_level_handler(driver, device, cmd) local level = math.floor(cmd.args.level / 100.0 * 254) @@ -40,7 +36,7 @@ local osram_iqbr30 = { [SwitchLevel.commands.setLevel.NAME] = set_switch_level_handler } }, - can_handle = can_handle_osram_iqbr30 + can_handle = require("zigbee-dimming-light.osram-iqbr30.can_handle"), } return osram_iqbr30 diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua new file mode 100644 index 0000000000..6251d62343 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua @@ -0,0 +1,6 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("zigbee-dimming-light.osram-iqbr30"), + lazy_load("zigbee-dimming-light.zll-dimmer") +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua new file mode 100644 index 0000000000..cba10b4d8a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua @@ -0,0 +1,9 @@ +return function(opts, driver, device) + local ZLL_DIMMER_FINGERPRINTS = require("zigbee-dimming-light.zll-dimmer.fingerprints") + for _, fingerprint in ipairs(ZLL_DIMMER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-dimming-light.zll-dimmer") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua new file mode 100644 index 0000000000..c41f026088 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua @@ -0,0 +1,8 @@ +return { + {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch + {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer + {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch + {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch + {mfr = "MRVL", model = "MZ100"}, + {mfr = "CREE", model = "Connected A-19 60W Equivalent"} +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua index 72b35fd580..e42846b6a9 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua @@ -19,24 +19,6 @@ local Level = clusters.Level local SwitchLevel = capabilities.switchLevel -local ZLL_DIMMER_FINGERPRINTS = { - {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch - {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer - {mfr = "Leviton", model = "DL1KD"}, -- Leviton Lumina RF Dimmer Switch - {mfr = "Leviton", model = "ZSD07"}, -- Leviton Lumina RF 0-10V Dimming Wall Switch - {mfr = "MRVL", model = "MZ100"}, - {mfr = "CREE", model = "Connected A-19 60W Equivalent"} -} - -local function can_handle_zll_dimmer(opts, driver, device) - for _, fingerprint in ipairs(ZLL_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function set_switch_level_handler(driver, device, cmd) local level = math.floor(cmd.args.level / 100.0 * 254) @@ -51,7 +33,7 @@ local zll_dimmer = { [SwitchLevel.commands.setLevel.NAME] = set_switch_level_handler } }, - can_handle = can_handle_zll_dimmer + can_handle = require("zigbee-dimming-light.zll-dimmer.can_handle") } return zll_dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua new file mode 100644 index 0000000000..37c655bee3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua @@ -0,0 +1,13 @@ +local ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT = { + {mfr = "Aurora", model = "DoubleSocket50AU"} +} + +return function(opts, driver, device, ...) + for _, fingerprint in ipairs(ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-dual-metering-switch") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua index 68f7d63675..981a4b5400 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua @@ -21,20 +21,6 @@ local configurations = require "configurations" local CHILD_ENDPOINT = 2 -local ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT = { - {mfr = "Aurora", model = "DoubleSocket50AU"} -} - -local function can_handle_zigbee_dual_metering_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-dual-metering-switch") - return true, subdriver - end - end - return false -end - local function do_refresh(self, device) device:send(OnOff.attributes.OnOff:read(device)) device:send(ElectricalMeasurement.attributes.ActivePower:read(device)) @@ -80,7 +66,7 @@ local zigbee_dual_metering_switch = { init = configurations.power_reconfig_wrapper(device_init), added = device_added }, - can_handle = can_handle_zigbee_dual_metering_switch + can_handle = require("zigbee-dual-metering-switch.can_handle"), } return zigbee_dual_metering_switch diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua new file mode 100644 index 0000000000..56106764b6 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua @@ -0,0 +1,9 @@ +return function(opts, driver, device, ...) + local can_handle = device:get_manufacturer() == "DAWON_DNS" + if can_handle then + local subdriver = require("zigbee-metering-plug-power-consumption-report") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 044bf509df..7da5a400c7 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -54,15 +54,7 @@ local zigbee_metering_plug_power_conumption_report = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - local can_handle = device:get_manufacturer() == "DAWON_DNS" - if can_handle then - local subdriver = require("zigbee-metering-plug-power-consumption-report") - return true, subdriver - else - return false - end - end + can_handle = require("zigbee-metering-plug-power-consumption-report.can_handle"), } return zigbee_metering_plug_power_conumption_report diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua new file mode 100644 index 0000000000..ee01893636 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua @@ -0,0 +1,14 @@ + +return function(opts, driver, device) + local AURORA_RELAY_FINGERPRINTS = { + { mfr = "Aurora", model = "Smart16ARelay51AU" }, + { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, + { mfr = "SALUS", model = "SX885ZB" } + } + for _, fingerprint in ipairs(AURORA_RELAY_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-switch-power.aurora-relay") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua index 0f8eac96bc..42fa53e407 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua @@ -14,21 +14,6 @@ local constants = require "st.zigbee.constants" -local AURORA_RELAY_FINGERPRINTS = { - { mfr = "Aurora", model = "Smart16ARelay51AU" }, - { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, - { mfr = "SALUS", model = "SX885ZB" } -} - -local function can_handle_aurora_relay(opts, driver, device) - for _, fingerprint in ipairs(AURORA_RELAY_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function do_configure(driver, device) device:configure() @@ -43,7 +28,7 @@ local aurora_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_aurora_relay + can_handle = require("zigbee-switch-power.aurora-relay.can_handle"), } return aurora_relay diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua new file mode 100644 index 0000000000..788e0362ec --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local SWITCH_POWER_FINGERPRINTS = require "zigbee-switch-power.fingerprints" + for _, fingerprint in ipairs(SWITCH_POWER_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("zigbee-switch-power") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua new file mode 100644 index 0000000000..902b7e4937 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua @@ -0,0 +1,15 @@ +return { + { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" }, + { model = "PAN18-v1.0.7" }, + { model = "E210-KR210Z1-HA" }, + { mfr = "Aurora", model = "Smart16ARelay51AU" }, + { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, + { mfr = "Jasco Products", model = "45853" }, + { mfr = "Jasco Products", model = "45856" }, + { mfr = "MEGAMAN", model = "SH-PSUKC44B-E" }, + { mfr = "ClimaxTechnology", model = "PSM_00.00.00.35TC" }, + { mfr = "SALUS", model = "SX885ZB" }, + { mfr = "AduroSmart Eria", model = "AD-SmartPlug3001" }, + { mfr = "AduroSmart Eria", model = "BPU3" }, + { mfr = "AduroSmart Eria", model = "BDP3001" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua index d9ae8f4750..9a32274d0a 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua @@ -19,32 +19,6 @@ local constants = require "st.zigbee.constants" local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement -local SWITCH_POWER_FINGERPRINTS = { - { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" }, - { model = "PAN18-v1.0.7" }, - { model = "E210-KR210Z1-HA" }, - { mfr = "Aurora", model = "Smart16ARelay51AU" }, - { mfr = "Develco Products A/S", model = "Smart16ARelay51AU" }, - { mfr = "Jasco Products", model = "45853" }, - { mfr = "Jasco Products", model = "45856" }, - { mfr = "MEGAMAN", model = "SH-PSUKC44B-E" }, - { mfr = "ClimaxTechnology", model = "PSM_00.00.00.35TC" }, - { mfr = "SALUS", model = "SX885ZB" }, - { mfr = "AduroSmart Eria", model = "AD-SmartPlug3001" }, - { mfr = "AduroSmart Eria", model = "BPU3" }, - { mfr = "AduroSmart Eria", model = "BDP3001" } -} - -local function can_handle_zigbee_switch_power(opts, driver, device) - for _, fingerprint in ipairs(SWITCH_POWER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("zigbee-switch-power") - return true, subdriver - end - end - return false -end - local function active_power_meter_handler(driver, device, value, zb_rx) local raw_value = value.value local divisor = device:get_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) or 10 @@ -75,11 +49,8 @@ local zigbee_switch_power = { } } }, - sub_drivers = { - require("zigbee-switch-power/aurora-relay"), - require("zigbee-switch-power/vimar") - }, - can_handle = can_handle_zigbee_switch_power + sub_drivers = require("zigbee-switch-power.sub_drivers"), + can_handle = require("zigbee-switch-power.can_handle"), } return zigbee_switch_power diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua new file mode 100644 index 0000000000..b9c6316ba8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua @@ -0,0 +1,6 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("zigbee-switch-power.aurora-relay"), + lazy_load("zigbee-switch-power.vimar") +} diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua new file mode 100644 index 0000000000..8580ce38a8 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua @@ -0,0 +1,12 @@ + +return function(opts, driver, device) + local VIMAR_FINGERPRINTS = { + { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" } + } + for _, fingerprint in ipairs(VIMAR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-switch-power.vimar") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua index ed65ce785b..3a4c73d94a 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua @@ -18,19 +18,6 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local ElectricalMeasurement = zcl_clusters.ElectricalMeasurement -local VIMAR_FINGERPRINTS = { - { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" } -} - -local function can_handle_vimar_switch_power(opts, driver, device) - for _, fingerprint in ipairs(VIMAR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local function do_configure(driver, device) device:configure() device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1, {persist = true}) @@ -45,7 +32,7 @@ local vimar_switch_power = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_vimar_switch_power + can_handle = require("zigbee-switch-power.vimar.can_handle"), } return vimar_switch_power diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua new file mode 100644 index 0000000000..354cd3bf9f --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua @@ -0,0 +1,10 @@ +return function(opts, driver, device) + local ZLL_DIMMER_BULB_FINGERPRINTS = require "zll-dimmer-bulb.fingerprints" + local can_handle = (ZLL_DIMMER_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] + if can_handle then + local subdriver = require("zll-dimmer-bulb") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua new file mode 100644 index 0000000000..9ff7bb0618 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua @@ -0,0 +1,114 @@ +return { + ["AduroSmart Eria"] = { + ["ZLL-DimmableLight"] = true, + ["ZLL-ExtendedColor"] = true, + ["ZLL-ColorTemperature"] = true + }, + ["IKEA of Sweden"] = { + ["TRADFRI bulb E26 opal 1000lm"] = true, + ["TRADFRI bulb E12 W op/ch 400lm"] = true, + ["TRADFRI bulb E17 W op/ch 400lm"] = true, + ["TRADFRI bulb GU10 W 400lm"] = true, + ["TRADFRI bulb E27 W opal 1000lm"] = true, + ["TRADFRI bulb E26 W opal 1000lm"] = true, + ["TRADFRI bulb E14 W op/ch 400lm"] = true, + ["TRADFRI transformer 10W"] = true, + ["TRADFRI Driver 10W"] = true, + ["TRADFRI transformer 30W"] = true, + ["TRADFRI Driver 30W"] = true, + ["TRADFRI bulb E26 WS clear 950lm"] = true, + ["TRADFRI bulb GU10 WS 400lm"] = true, + ["TRADFRI bulb E12 WS opal 400lm"] = true, + ["TRADFRI bulb E26 WS opal 980lm"] = true, + ["TRADFRI bulb E27 WS clear 950lm"] = true, + ["TRADFRI bulb E14 WS opal 400lm"] = true, + ["TRADFRI bulb E27 WS opal 980lm"] = true, + ["FLOALT panel WS 30x30"] = true, + ["FLOALT panel WS 30x90"] = true, + ["FLOALT panel WS 60x60"] = true, + ["SURTE door WS 38x64"] = true, + ["JORMLIEN door WS 40x80"] = true, + ["TRADFRI bulb E27 CWS opal 600lm"] = true, + ["TRADFRI bulb E26 CWS opal 600lm"] = true + }, + ["Eaton"] = { + ["Halo_RL5601"] = true + }, + ["Megaman"] = { + ["ZLL-DimmableLight"] = true, + ["ZLL-ExtendedColor"] = true + }, + ["MEGAMAN"] = { + ["BSZTM002"] = true, + ["BSZTM003"] = true + }, + ["innr"] = { + ["RS 125"] = true, + ["RB 165"] = true, + ["RB 175 W"] = true, + ["RB 145"] = true, + ["RS 128 T"] = true, + ["RB 178 T"] = true, + ["RB 148 T"] = true, + ["RB 185 C"] = true, + ["FL 130 C"] = true, + ["OFL 120 C"] = true, + ["OFL 140 C"] = true, + ["OSL 130 C"] = true + }, + ["Leviton"] = { + ["DG3HL"] = true, + ["DG6HD"] = true + }, + ["OSRAM"] = { + ["Classic A60 W clear"] = true, + ["Classic A60 W clear - LIGHTIFY"] = true, + ["CLA60 OFD OSRAM"] = true, + ["Classic A60 RGBW"] = true, + ["PAR 16 50 RGBW - LIGHTIFY"] = true, + ["CLA60 RGBW OSRAM"] = true, + ["Flex RGBW"] = true, + ["Gardenpole RGBW-Lightify"] = true, + ["LIGHTIFY Outdoor Flex RGBW"] = true, + ["LIGHTIFY Indoor Flex RGBW"] = true, + ["Classic B40 TW - LIGHTIFY"] = true, + ["CLA60 TW OSRAM"] = true + }, + ["Philips"] = { + ["LWB006"] = true, + ["LWB007"] = true, + ["LWB010"] = true, + ["LWB014"] = true, + ["LCT001"] = true, + ["LCT002"] = true, + ["LCT003"] = true, + ["LCT007"] = true, + ["LCT010"] = true, + ["LCT011"] = true, + ["LCT012"] = true, + ["LCT014"] = true, + ["LCT015"] = true, + ["LCT016"] = true, + ["LST001"] = true, + ["LST002"] = true, + ["LTW001"] = true, + ["LTW004"] = true, + ["LTW010"] = true, + ["LTW011"] = true, + ["LTW012"] = true, + ["LTW013"] = true, + ["LTW014"] = true, + ["LTW015"] = true + }, + ["sengled"] = { + ["E14-U43"] = true, + ["E13-N11"] = true + }, + ["GLEDOPTO"] = { + ["GL-C-008"] = true, + ["GL-B-001Z"] = true + }, + ["Ubec"] = { + ["BBB65L-HY"] = true + } +} diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua index 0f35666fe0..2347345f49 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua @@ -19,131 +19,6 @@ local colorTemperature_defaults = require "st.zigbee.defaults.colorTemperature_d local OnOff = clusters.OnOff local Level = clusters.Level -local ZLL_DIMMER_BULB_FINGERPRINTS = { - ["AduroSmart Eria"] = { - ["ZLL-DimmableLight"] = true, - ["ZLL-ExtendedColor"] = true, - ["ZLL-ColorTemperature"] = true - }, - ["IKEA of Sweden"] = { - ["TRADFRI bulb E26 opal 1000lm"] = true, - ["TRADFRI bulb E12 W op/ch 400lm"] = true, - ["TRADFRI bulb E17 W op/ch 400lm"] = true, - ["TRADFRI bulb GU10 W 400lm"] = true, - ["TRADFRI bulb E27 W opal 1000lm"] = true, - ["TRADFRI bulb E26 W opal 1000lm"] = true, - ["TRADFRI bulb E14 W op/ch 400lm"] = true, - ["TRADFRI transformer 10W"] = true, - ["TRADFRI Driver 10W"] = true, - ["TRADFRI transformer 30W"] = true, - ["TRADFRI Driver 30W"] = true, - ["TRADFRI bulb E26 WS clear 950lm"] = true, - ["TRADFRI bulb GU10 WS 400lm"] = true, - ["TRADFRI bulb E12 WS opal 400lm"] = true, - ["TRADFRI bulb E26 WS opal 980lm"] = true, - ["TRADFRI bulb E27 WS clear 950lm"] = true, - ["TRADFRI bulb E14 WS opal 400lm"] = true, - ["TRADFRI bulb E27 WS opal 980lm"] = true, - ["FLOALT panel WS 30x30"] = true, - ["FLOALT panel WS 30x90"] = true, - ["FLOALT panel WS 60x60"] = true, - ["SURTE door WS 38x64"] = true, - ["JORMLIEN door WS 40x80"] = true, - ["TRADFRI bulb E27 CWS opal 600lm"] = true, - ["TRADFRI bulb E26 CWS opal 600lm"] = true - }, - ["Eaton"] = { - ["Halo_RL5601"] = true - }, - ["Megaman"] = { - ["ZLL-DimmableLight"] = true, - ["ZLL-ExtendedColor"] = true - }, - ["MEGAMAN"] = { - ["BSZTM002"] = true, - ["BSZTM003"] = true - }, - ["innr"] = { - ["RS 125"] = true, - ["RB 165"] = true, - ["RB 175 W"] = true, - ["RB 145"] = true, - ["RS 128 T"] = true, - ["RB 178 T"] = true, - ["RB 148 T"] = true, - ["RB 185 C"] = true, - ["FL 130 C"] = true, - ["OFL 120 C"] = true, - ["OFL 140 C"] = true, - ["OSL 130 C"] = true - }, - ["Leviton"] = { - ["DG3HL"] = true, - ["DG6HD"] = true - }, - ["OSRAM"] = { - ["Classic A60 W clear"] = true, - ["Classic A60 W clear - LIGHTIFY"] = true, - ["CLA60 OFD OSRAM"] = true, - ["Classic A60 RGBW"] = true, - ["PAR 16 50 RGBW - LIGHTIFY"] = true, - ["CLA60 RGBW OSRAM"] = true, - ["Flex RGBW"] = true, - ["Gardenpole RGBW-Lightify"] = true, - ["LIGHTIFY Outdoor Flex RGBW"] = true, - ["LIGHTIFY Indoor Flex RGBW"] = true, - ["Classic B40 TW - LIGHTIFY"] = true, - ["CLA60 TW OSRAM"] = true - }, - ["Philips"] = { - ["LWB006"] = true, - ["LWB007"] = true, - ["LWB010"] = true, - ["LWB014"] = true, - ["LCT001"] = true, - ["LCT002"] = true, - ["LCT003"] = true, - ["LCT007"] = true, - ["LCT010"] = true, - ["LCT011"] = true, - ["LCT012"] = true, - ["LCT014"] = true, - ["LCT015"] = true, - ["LCT016"] = true, - ["LST001"] = true, - ["LST002"] = true, - ["LTW001"] = true, - ["LTW004"] = true, - ["LTW010"] = true, - ["LTW011"] = true, - ["LTW012"] = true, - ["LTW013"] = true, - ["LTW014"] = true, - ["LTW015"] = true - }, - ["sengled"] = { - ["E14-U43"] = true, - ["E13-N11"] = true - }, - ["GLEDOPTO"] = { - ["GL-C-008"] = true, - ["GL-B-001Z"] = true - }, - ["Ubec"] = { - ["BBB65L-HY"] = true - } -} - -local function can_handle_zll_dimmer_bulb(opts, driver, device) - local can_handle = (ZLL_DIMMER_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] - if can_handle then - local subdriver = require("zll-dimmer-bulb") - return true, subdriver - else - return false - end -end - local function do_configure(driver, device) device:configure() end @@ -204,7 +79,7 @@ local zll_dimmer_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = handle_set_color_temperature } }, - can_handle = can_handle_zll_dimmer_bulb + can_handle = require("zll-dimmer-bulb.can_handle"), } return zll_dimmer_bulb diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua new file mode 100644 index 0000000000..3ddd9b8328 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua @@ -0,0 +1,11 @@ +return function(opts, driver, device, zb_rx, ...) + local constants = require "st.zigbee.constants" + + local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] + if (endpoint ~= nil and endpoint.profile_id == constants.ZLL_PROFILE_ID) then + local subdriver = require("zll-polling") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index 1fa972ca98..ad619e070e 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -14,17 +14,6 @@ local device_lib = require "st.device" local clusters = require "st.zigbee.zcl.clusters" -local ZLL_PROFILE_ID = 0xC05E - -local function zll_profile(opts, driver, device, zb_rx, ...) - local endpoint = device.zigbee_endpoints[device.fingerprinted_endpoint_id] or device.zigbee_endpoints[tostring(device.fingerprinted_endpoint_id)] - if (endpoint ~= nil and endpoint.profile_id == ZLL_PROFILE_ID) then - local subdriver = require("zll-polling") - return true, subdriver - else - return false - end -end local function set_up_zll_polling(driver, device) local INFREQUENT_POLL_COUNTER = "_infrequent_poll_counter" @@ -57,7 +46,7 @@ local ZLL_polling = { lifecycle_handlers = { init = set_up_zll_polling }, - can_handle = zll_profile + can_handle = require("zll-polling.can_handle"), } -return ZLL_polling \ No newline at end of file +return ZLL_polling From d42421a180b52eae1c557bed10ddf53a26704ec8 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Mon, 3 Nov 2025 15:25:41 -0600 Subject: [PATCH 233/449] Update copywrites to be shorter and add them where missing This is done to maintain consitency across the whole driver and set an example for other drivers to follow. The reduced source file comment should also help with memory savings since comments do affect memory of a driver when the driver code is being loaded into the Lua runtime. --- .../zigbee-switch/src/aqara-light/can_handle.lua | 3 +++ .../zigbee-switch/src/aqara-light/init.lua | 3 +++ .../zigbee-switch/src/aqara/can_handle.lua | 3 +++ .../zigbee-switch/src/aqara/fingerprints.lua | 3 +++ .../SmartThings/zigbee-switch/src/aqara/init.lua | 3 +++ .../src/aqara/multi-switch/can_handle.lua | 3 +++ .../src/aqara/multi-switch/fingerprints.lua | 3 +++ .../src/aqara/multi-switch/init.lua | 3 +++ .../zigbee-switch/src/aqara/sub_drivers.lua | 3 +++ .../src/aqara/version/can_handle.lua | 3 +++ .../zigbee-switch/src/aqara/version/init.lua | 3 +++ .../src/bad_on_off_data_type/can_handle.lua | 3 +++ .../src/bad_on_off_data_type/init.lua | 15 ++------------- .../zigbee-switch/src/configurations/devices.lua | 15 ++------------- .../zigbee-switch/src/configurations/init.lua | 15 ++------------- .../zigbee-switch/src/ezex/can_handle.lua | 3 +++ .../SmartThings/zigbee-switch/src/ezex/init.lua | 15 ++------------- .../zigbee-switch/src/frient/can_handle.lua | 3 +++ .../zigbee-switch/src/frient/fingerprints.lua | 3 +++ .../zigbee-switch/src/frient/init.lua | 15 ++------------- .../src/ge-link-bulb/can_handle.lua | 3 +++ .../zigbee-switch/src/ge-link-bulb/init.lua | 15 ++------------- .../zigbee-switch/src/hanssem/can_handle.lua | 3 +++ .../zigbee-switch/src/hanssem/fingerprints.lua | 3 +++ .../zigbee-switch/src/hanssem/init.lua | 15 ++------------- .../src/ikea-xy-color-bulb/can_handle.lua | 3 +++ .../src/ikea-xy-color-bulb/init.lua | 15 ++------------- drivers/SmartThings/zigbee-switch/src/init.lua | 15 ++------------- .../zigbee-switch/src/inovelli/can_handle.lua | 3 +++ .../zigbee-switch/src/inovelli/common.lua | 15 ++------------- .../zigbee-switch/src/inovelli/init.lua | 15 ++------------- .../zigbee-switch/src/inovelli/sub_drivers.lua | 3 +++ .../src/inovelli/vzm32-sn/can_handle.lua | 3 +++ .../zigbee-switch/src/inovelli/vzm32-sn/init.lua | 15 ++------------- .../zigbee-switch/src/jasco/can_handle.lua | 3 +++ .../SmartThings/zigbee-switch/src/jasco/init.lua | 15 ++------------- .../zigbee-switch/src/laisiao/can_handle.lua | 3 +++ .../zigbee-switch/src/laisiao/init.lua | 15 ++------------- .../zigbee-switch/src/lazy_load_subdriver.lua | 3 +++ .../src/lifecycle_handlers/device_added.lua | 15 ++------------- .../src/lifecycle_handlers/do_configure.lua | 15 ++------------- .../src/lifecycle_handlers/find_child.lua | 15 ++------------- .../src/lifecycle_handlers/info_changed.lua | 15 ++------------- .../src/multi-switch-no-master/can_handle.lua | 3 +++ .../src/multi-switch-no-master/fingerprints.lua | 3 +++ .../src/multi-switch-no-master/init.lua | 15 ++------------- .../src/non_zigbee_devices/can_handle.lua | 3 +++ .../src/non_zigbee_devices/init.lua | 15 ++------------- .../zigbee-switch/src/preferences.lua | 15 ++------------- .../zigbee-switch/src/rexense/can_handle.lua | 3 +++ .../zigbee-switch/src/rexense/init.lua | 15 ++------------- .../zigbee-switch/src/rgb-bulb/can_handle.lua | 3 +++ .../zigbee-switch/src/rgb-bulb/init.lua | 15 ++------------- .../zigbee-switch/src/rgbw-bulb/can_handle.lua | 3 +++ .../zigbee-switch/src/rgbw-bulb/fingerprints.lua | 3 +++ .../zigbee-switch/src/rgbw-bulb/init.lua | 15 ++------------- .../zigbee-switch/src/robb/can_handle.lua | 3 +++ .../SmartThings/zigbee-switch/src/robb/init.lua | 15 ++------------- .../src/sinope-dimmer/can_handle.lua | 3 +++ .../zigbee-switch/src/sinope-dimmer/init.lua | 15 ++------------- .../zigbee-switch/src/sinope/can_handle.lua | 3 +++ .../zigbee-switch/src/sinope/init.lua | 15 ++------------- .../zigbee-switch/src/switch_utils.lua | 15 ++------------- .../src/test/test_all_capability_zigbee_bulb.lua | 15 ++------------- .../src/test/test_aqara_led_bulb.lua | 15 ++------------- .../zigbee-switch/src/test/test_aqara_light.lua | 15 ++------------- .../src/test/test_aqara_smart_plug.lua | 15 ++------------- .../src/test/test_aqara_smart_plug_t1.lua | 15 ++------------- .../src/test/test_aqara_switch_module.lua | 15 ++------------- .../test/test_aqara_switch_module_no_power.lua | 15 ++------------- .../src/test/test_aqara_switch_no_power.lua | 15 ++------------- .../src/test/test_aqara_switch_power.lua | 15 ++------------- .../src/test/test_aqara_wall_switch.lua | 15 ++------------- .../zigbee-switch/src/test/test_aurora_relay.lua | 15 ++------------- .../src/test/test_bad_data_type.lua | 16 ++-------------- .../src/test/test_bad_device_kind.lua | 15 ++------------- .../zigbee-switch/src/test/test_cree_bulb.lua | 15 ++------------- .../src/test/test_duragreen_color_temp_bulb.lua | 15 ++------------- .../src/test/test_enbrighten_metering_dimmer.lua | 15 ++------------- .../src/test/test_frient_switch.lua | 15 ++------------- .../zigbee-switch/src/test/test_ge_link_bulb.lua | 15 ++------------- .../src/test/test_hanssem_switch.lua | 15 ++------------- .../src/test/test_inovelli_vzm31_sn.lua | 15 ++------------- .../src/test/test_inovelli_vzm31_sn_child.lua | 15 ++------------- .../test/test_inovelli_vzm31_sn_preferences.lua | 15 ++------------- .../src/test/test_inovelli_vzm32_sn.lua | 15 ++------------- .../src/test/test_inovelli_vzm32_sn_child.lua | 15 ++------------- .../test/test_inovelli_vzm32_sn_preferences.lua | 15 ++------------- .../zigbee-switch/src/test/test_jasco_switch.lua | 15 ++------------- .../src/test/test_laisiao_bath_heather.lua | 15 ++------------- .../zigbee-switch/src/test/test_multi_switch.lua | 15 ++------------- .../src/test/test_multi_switch_no_master.lua | 15 ++------------- .../src/test/test_multi_switch_power.lua | 15 ++------------- .../src/test/test_on_off_zigbee_bulb.lua | 15 ++------------- .../src/test/test_osram_iqbr30_light.lua | 15 ++------------- .../zigbee-switch/src/test/test_osram_light.lua | 15 ++------------- .../zigbee-switch/src/test/test_rgb_bulb.lua | 15 ++------------- .../zigbee-switch/src/test/test_rgbw_bulb.lua | 15 ++------------- .../src/test/test_robb_smarrt_2-wire_dimmer.lua | 15 ++------------- .../src/test/test_robb_smarrt_knob_dimmer.lua | 15 ++------------- .../src/test/test_sengled_color_temp_bulb.lua | 15 ++------------- ...st_sengled_dimmer_bulb_with_motion_sensor.lua | 15 ++------------- .../src/test/test_sinope_dimmer.lua | 15 ++------------- .../src/test/test_sinope_switch.lua | 15 ++------------- .../zigbee-switch/src/test/test_switch_power.lua | 15 ++------------- .../zigbee-switch/src/test/test_tuya_multi.lua | 15 ++------------- .../src/test/test_tuya_multi_switch.lua | 15 ++------------- .../src/test/test_wallhero_switch.lua | 15 ++------------- .../src/test/test_white_color_temp_bulb.lua | 15 ++------------- .../src/test/test_zigbee_ezex_switch.lua | 15 ++------------- ...ee_metering_plug_power_consumption_report.lua | 15 ++------------- .../test/test_zigbee_metering_plug_rexense.lua | 15 ++------------- .../src/test/test_zll_color_temp_bulb.lua | 15 ++------------- .../zigbee-switch/src/test/test_zll_dimmer.lua | 15 ++------------- .../src/test/test_zll_dimmer_bulb.lua | 15 ++------------- .../zigbee-switch/src/test/test_zll_rgb_bulb.lua | 15 ++------------- .../src/test/test_zll_rgbw_bulb.lua | 15 ++------------- .../zigbee-switch/src/tuya-multi/can_handle.lua | 3 +++ .../zigbee-switch/src/tuya-multi/init.lua | 3 +++ .../zigbee-switch/src/wallhero/can_handle.lua | 3 +++ .../zigbee-switch/src/wallhero/fingerprints.lua | 3 +++ .../zigbee-switch/src/wallhero/init.lua | 15 ++------------- .../src/white-color-temp-bulb/can_handle.lua | 3 +++ .../duragreen/can_handle.lua | 3 +++ .../src/white-color-temp-bulb/duragreen/init.lua | 15 ++------------- .../src/white-color-temp-bulb/fingerprints.lua | 3 +++ .../src/white-color-temp-bulb/init.lua | 15 ++------------- .../src/white-color-temp-bulb/sub_drivers.lua | 3 +++ .../zigbee-dimmer-power-energy/can_handle.lua | 3 +++ .../src/zigbee-dimmer-power-energy/init.lua | 15 ++------------- .../src/zigbee-dimming-light/can_handle.lua | 3 +++ .../src/zigbee-dimming-light/fingerprints.lua | 3 +++ .../src/zigbee-dimming-light/init.lua | 15 ++------------- .../osram-iqbr30/can_handle.lua | 3 +++ .../zigbee-dimming-light/osram-iqbr30/init.lua | 15 ++------------- .../src/zigbee-dimming-light/sub_drivers.lua | 3 +++ .../zll-dimmer/can_handle.lua | 3 +++ .../zll-dimmer/fingerprints.lua | 3 +++ .../src/zigbee-dimming-light/zll-dimmer/init.lua | 15 ++------------- .../zigbee-dual-metering-switch/can_handle.lua | 3 +++ .../src/zigbee-dual-metering-switch/init.lua | 15 ++------------- .../can_handle.lua | 3 +++ .../init.lua | 15 ++------------- .../aurora-relay/can_handle.lua | 3 +++ .../zigbee-switch-power/aurora-relay/init.lua | 15 ++------------- .../src/zigbee-switch-power/can_handle.lua | 3 +++ .../src/zigbee-switch-power/fingerprints.lua | 3 +++ .../src/zigbee-switch-power/init.lua | 15 ++------------- .../src/zigbee-switch-power/sub_drivers.lua | 3 +++ .../src/zigbee-switch-power/vimar/can_handle.lua | 3 +++ .../src/zigbee-switch-power/vimar/init.lua | 15 ++------------- .../src/zll-dimmer-bulb/can_handle.lua | 3 +++ .../src/zll-dimmer-bulb/fingerprints.lua | 3 +++ .../zigbee-switch/src/zll-dimmer-bulb/init.lua | 15 ++------------- .../zigbee-switch/src/zll-polling/can_handle.lua | 3 +++ .../zigbee-switch/src/zll-polling/init.lua | 15 ++------------- 156 files changed, 372 insertions(+), 1249 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua index 1900934725..73ea4c3d00 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local FINGERPRINTS = { { mfr = "LUMI", model = "lumi.light.acn004" }, diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index ec6e178f42..760ffebebf 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua index 312e3a7e08..e183f78266 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + 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 837b3243b8..63f94e2be3 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "LUMI", model = "lumi.plug.maeu01" }, { mfr = "LUMI", model = "lumi.plug.macn01" }, diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua index c4acb83a94..0c64b594fe 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" 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 26eb425583..1524296cff 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,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + 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 e8bfe86866..71ed1b26b3 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- 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" }, 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 cf143091e7..9280a30d0d 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_lib = require "st.device" local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua index cf4fec5466..d064785cb4 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua index 780b53a434..d56744fefa 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function (opts, driver, device) local PRIVATE_MODE = "PRIVATE_MODE" local private_mode = device:get_field(PRIVATE_MODE) or 0 diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua index 6e3b5ab125..bca3e9fb88 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/version/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local constants = require "st.zigbee.constants" diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua index 7b9510968d..49b42ddb35 100644 --- a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- There are reports of at least one device (SONOFF 01MINIZB) which occasionally -- reports this value as an Int8, rather than a Boolean, as per the spec diff --git a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua index 863d1e4ec4..2a59166bbb 100644 --- a/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/bad_on_off_data_type/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua index 51fd1c3665..9910dde229 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local ColorControl = clusters.ColorControl diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua index 2e1c4e2fc9..01f42abf91 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local CONFIGURATION_VERSION_KEY = "_configuration_version" diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua index 454f140209..4bb61719ff 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local ZIGBEE_METERING_SWITCH_FINGERPRINTS = { { model = "E240-KR116Z-HA" } diff --git a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua index 7f338e1b11..6c2d9e45e3 100644 --- a/drivers/SmartThings/zigbee-switch/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ezex/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zigbee_constants = require "st.zigbee.constants" local configurations = require "configurations" diff --git a/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua index 1c23272f30..2892763034 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Function to determine if the driver can handle this device return function(opts, driver, device, ...) local FRIENT_SMART_PLUG_FINGERPRINTS = require("frient.fingerprints") diff --git a/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua index a9c7333473..e2ac5ceaab 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "frient A/S", model = "SPLZB-131" }, { mfr = "frient A/S", model = "SPLZB-132" }, diff --git a/drivers/SmartThings/zigbee-switch/src/frient/init.lua b/drivers/SmartThings/zigbee-switch/src/frient/init.lua index af85f237a6..a615c721f4 100644 --- a/drivers/SmartThings/zigbee-switch/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/frient/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua index 08ce9c471f..6addbc463c 100644 --- a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local GE_LINK_BULB_FINGERPRINTS = { ["GE_Appliances"] = { diff --git a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua index 51bab5110f..b1f5ea8508 100644 --- a/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ge-link-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua index de9c477317..9f9f1c4ace 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local FINGERPRINTS = require "hanssem.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua index 67d2f40022..65c4421053 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "Winners", model = "LSS1-101", children = 0 }, { mfr = "Winners", model = "LSS1-102", children = 1 }, diff --git a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua index e1f9939946..083893448b 100644 --- a/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/hanssem/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local stDevice = require "st.device" local configurations = require "configurations" diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua index 3c507a2627..47992c0559 100644 --- a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local IKEA_XY_COLOR_BULB_FINGERPRINTS = { diff --git a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua index 193d032336..a026ac6564 100644 --- a/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/ikea-xy-color-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 8a7a05a9cb..696ff8ada9 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- The only reason we need this is because of supported_capabilities on the driver template local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua index 325c2bc973..473c1cb807 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local INOVELLI_FINGERPRINTS = { diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua index c80d0deca9..33d3ff577c 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index dd261cdd8a..73967b41f3 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua index c7f0c32bfc..20677e4764 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua index e2496028a3..aa0004b193 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local INOVELLI_VZM32_SN_FINGERPRINTS = { diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua index 6b46885553..edadf85b90 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua index be9bd75694..c97ff700c8 100644 --- a/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/jasco/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local JASCO_SWTICH_FINGERPRINTS = { { mfr = "Jasco Products", model = "43095" }, diff --git a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua index 250903d46a..a21059dfb4 100644 --- a/drivers/SmartThings/zigbee-switch/src/jasco/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/jasco/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua index 991547f9d1..7ed921844b 100644 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local FINGERPRINTS = { { mfr = "LAISIAO", model = "yuba" }, diff --git a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua index 5a26e01f34..2833843793 100755 --- a/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/laisiao/init.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua index 80dc2101ae..0bee6d2a75 100644 --- a/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua +++ b/drivers/SmartThings/zigbee-switch/src/lazy_load_subdriver.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(sub_driver_name) -- gets the current lua libs api version local version = require "version" diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua index 275b7e4b27..a241ea506c 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/device_added.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local find_child = require "lifecycle_handlers.find_child" diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua index 088cc604b4..465969a755 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/do_configure.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" return function(self, device) diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua index e33df67817..31b1640619 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/find_child.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 return function(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) diff --git a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua index 9699760a92..bd15064b5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua +++ b/drivers/SmartThings/zigbee-switch/src/lifecycle_handlers/info_changed.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 return function(self, device, event, args) local preferences = require "preferences" diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua index d780d70d06..80cd735e93 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local FINGERPRINTS = require "multi-switch-no-master.fingerprints" for _, fingerprint in ipairs(FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua index f8bb368525..ca54566d8d 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "DAWON_DNS", model = "PM-S240-ZB", children = 1 }, { mfr = "DAWON_DNS", model = "PM-S240R-ZB", children = 1 }, diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua index 85bfb6ae96..ba0ed07ae3 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local utils = require "st.utils" local configurations = require "configurations" diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua index ce1a124104..bc394d4fa0 100644 --- a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local st_device = require "st.device" diff --git a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua index 628eb825ff..63876e658a 100644 --- a/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/non_zigbee_devices/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- This is a patch for the zigbee-switch driver to fix https://smartthings.atlassian.net/browse/CHAD-16558 -- Several hubs were found that had zigbee switch drivers hosting zwave devices. diff --git a/drivers/SmartThings/zigbee-switch/src/preferences.lua b/drivers/SmartThings/zigbee-switch/src/preferences.lua index f0170aaf40..9b8a0cb66d 100644 --- a/drivers/SmartThings/zigbee-switch/src/preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua index 7fee03f115..bcdc666417 100644 --- a/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/rexense/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local ZIGBEE_METERING_PLUG_FINGERPRINTS = { { mfr = "REXENSE", model = "HY0105" } -- HONYAR Outlet" diff --git a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua index 598afa75ec..4b16afe3b0 100644 --- a/drivers/SmartThings/zigbee-switch/src/rexense/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rexense/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua index 6d255174d6..e52cf32dcd 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local RGB_BULB_FINGERPRINTS = { ["OSRAM"] = { diff --git a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua index 03ed2b1574..867b069bfe 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgb-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua index 77c16d7b90..724bb6266a 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local RGBW_BULB_FINGERPRINTS = require "rgbw-bulb.fingerprints" local can_handle = (RGBW_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua index e0c8c3dd93..c8eedcfec8 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { ["Samsung Electronics"] = { ["SAMSUNG-ITM-Z-002"] = true diff --git a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua index aa9b25e9ae..cfa2c81855 100644 --- a/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/rgbw-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua index 4544c17d64..480bc83ab5 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local ROBB_DIMMER_FINGERPRINTS = { { mfr = "ROBB smarrt", model = "ROB_200-011-0" }, diff --git a/drivers/SmartThings/zigbee-switch/src/robb/init.lua b/drivers/SmartThings/zigbee-switch/src/robb/init.lua index 9081d058db..1a8c2948c3 100644 --- a/drivers/SmartThings/zigbee-switch/src/robb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/robb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" local zcl_clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua index 326de13801..3a7233e6b9 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "DM2500ZB" if can_handle then diff --git a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua index c2aba06bda..d97c873467 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua index 90b7965184..624baee756 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local can_handle = device:get_manufacturer() == "Sinope Technologies" and device:get_model() == "SW2500ZB" if can_handle then diff --git a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua index a7101bb971..af0300a377 100644 --- a/drivers/SmartThings/zigbee-switch/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/sinope/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 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" diff --git a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua index 7f5429dceb..66ad4715f9 100644 --- a/drivers/SmartThings/zigbee-switch/src/switch_utils.lua +++ b/drivers/SmartThings/zigbee-switch/src/switch_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local switch_utils = {} 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 319066fa3b..70dfc96780 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 9c7a7cc0e5..8584e74e1d 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 68f54c986f..814de3f055 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d9edfbe425..0d51bd11c2 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" 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 c1ba5bef7d..8a544caf13 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" 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 e0038602fb..f27d84cfc6 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" 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 6ed61191da..840aadacb9 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 e35a1c25e5..207e7bf21b 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 8b596191a5..a02ef37aa2 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b47d0595c8..3711116d99 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 6dd6ed75b7..57d6576955 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 69ee7f043a..b30f49df23 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 @@ -1,17 +1,5 @@ - --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 3f66d675c9..bd8ce15b10 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 e971201f85..8841c5a53d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 0dee55ebf4..55c491e77d 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 3b8c7187b4..3b5a5b93b1 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 8b9b26ba13..4290d6ebbe 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" 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 cdcd49c616..d37dafe45d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 37de10bdec..96f4581310 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 65a3084022..d70159dde5 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b6c6c86a38..c6476cba5e 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 a9e8850b7d..64b0d51ee5 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 452b98454d..debdeba110 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d660e8c501..26346c04a9 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 9bb0a07147..28ae4829a0 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 750ef895f2..52ba3ef2a6 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 ec90d234d8..0457c0eb62 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local clusters = require "st.zigbee.zcl.clusters" 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 4b5a10ecfb..1ff11bd754 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 6d1cf40e63..6603a0831c 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 c9e16aadad..4e35d5067d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 51ea4fe9c6..7f0d412016 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 aa366750d8..082aa0c5c2 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 3efe35fc49..8e2e847e2c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 5f7e37f5eb..68af9fdedb 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 61a9fcf1ac..24ec471c9d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d533d69489..1c815e2d7c 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 d7e4db0f09..3ed587916c 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 c528e22c92..e2ddba42d3 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 2ccaf7be36..e28c4de0b6 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 d1aadf29a3..0859138a8c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local base64 = require "st.base64" local cluster_base = require "st.zigbee.cluster_base" 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 35242deb6c..34e5f78640 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 d4d427bc58..04c17b15de 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 45f3d4e2b3..a00f74bddf 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 41924e891d..1ff11bd754 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 26e72466c9..ce1a9e2882 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local clusters = require "st.zigbee.zcl.clusters" 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 eea20eb184..f024aa45be 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 88470b5489..8995523952 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 5128aa1cb4..4514a90cbb 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 a3be084eb2..e1519a4d1a 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 fc68ec7786..1a13044f48 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 eb590c3874..71305fba20 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 a82dac084a..0ac17313d3 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 7bf6b175cd..3435b22288 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 7945b8d030..cf53f7e359 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua index f471c99151..c4878c95c2 100644 --- a/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local function is_multi_endpoint(device) local main_endpoint = device:get_endpoint(0x0006) for _, ep in ipairs(device.zigbee_endpoints) do diff --git a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua index b0c7b85202..51c91ad768 100644 --- a/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/tuya-multi/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua index d63ed62a49..69bcf73fce 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 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 diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua index c352b6feb4..9d58e6457a 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "WALL HERO", model = "ACL-401S4I", switches = 4, buttons = 0 }, { mfr = "WALL HERO", model = "ACL-401S8I", switches = 4, buttons = 4 }, diff --git a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua index 531a7c4bf2..1e0e7eb26e 100644 --- a/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/wallhero/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local log = require "log" diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua index 1b609d16ad..d45b7b745f 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local WHITE_COLOR_TEMP_BULB_FINGERPRINTS = require "white-color-temp-bulb.fingerprints" local can_handle = (WHITE_COLOR_TEMP_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua index 2b4c439e64..b3b8bd591b 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local DURAGREEN_BULB_FINGERPRINTS = { ["DURAGREEN"] = { diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua index 4c6d134e0e..b6f96842c3 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/duragreen/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua index 6824bf40ae..02bb28e88c 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { ["DURAGREEN"] = { ["DG-CW-02"] = true, diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua index 40b482604c..25bc13b65f 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua index 450c97b97b..9beaf743bc 100644 --- a/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/white-color-temp-bulb/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { lazy_load("white-color-temp-bulb.duragreen"), diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua index 7776397269..7b13c4b630 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZIGBEE_DIMMER_POWER_ENERGY_FINGERPRINTS = { { mfr = "Jasco Products", model = "43082" } } diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua index b3c1a61969..dc37bf1181 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimmer-power-energy/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua index 1c2f6d3f69..cbbc8dfeab 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local DIMMING_LIGHT_FINGERPRINTS = require "zigbee-dimming-light.fingerprints" for _, fingerprint in ipairs(DIMMING_LIGHT_FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua index b90edb17e8..820bd633b6 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = "Vimar", model = "DimmerSwitch_v1.0"}, -- Vimar Smart Dimmer Switch {mfr = "OSRAM", model = "LIGHTIFY A19 ON/OFF/DIM"}, -- SYLVANIA Smart A19 Soft White diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua index 9b05f3092f..dc8629c57b 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua index a5cf2faff0..8463ce2a77 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local res = device:get_manufacturer() == "OSRAM SYLVANIA" and device:get_model() == "iQBR30" if res then diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua index 43d22558b8..74d9acb903 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/osram-iqbr30/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua index 6251d62343..fa188a4e7e 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua index cba10b4d8a..d75b86b353 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local ZLL_DIMMER_FINGERPRINTS = require("zigbee-dimming-light.zll-dimmer.fingerprints") for _, fingerprint in ipairs(ZLL_DIMMER_FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua index c41f026088..1a5176bcd5 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = "Leviton", model = "DL6HD"}, -- Leviton Dimmer Switch {mfr = "Leviton", model = "DL3HL"}, -- Leviton Lumina RF Plug-In Dimmer diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua index e42846b6a9..039b049fc7 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dimming-light/zll-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua index 37c655bee3..fbc679fb18 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZIGBEE_DUAL_METERING_SWITCH_FINGERPRINT = { {mfr = "Aurora", model = "DoubleSocket50AU"} } diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua index 981a4b5400..b1d10f94a6 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-dual-metering-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local st_device = require "st.device" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua index 56106764b6..e6e3165aa3 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, ...) local can_handle = device:get_manufacturer() == "DAWON_DNS" if can_handle then diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 7da5a400c7..17dc0ee2bd 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua index ee01893636..18cff448c1 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local AURORA_RELAY_FINGERPRINTS = { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua index 42fa53e407..266ff9b4e7 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/aurora-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua index 788e0362ec..ed4db3dc52 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local SWITCH_POWER_FINGERPRINTS = require "zigbee-switch-power.fingerprints" for _, fingerprint in ipairs(SWITCH_POWER_FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua index 902b7e4937..d277d36967 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = "Vimar", model = "Mains_Power_Outlet_v1.0" }, { model = "PAN18-v1.0.7" }, diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua index 9a32274d0a..0ea2224a6f 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua index b9c6316ba8..340e1f27c6 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua index 8580ce38a8..8143fb6a8e 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local VIMAR_FINGERPRINTS = { diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua index 3a4c73d94a..82eb0674d4 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-switch-power/vimar/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local constants = require "st.zigbee.constants" local device_management = require "st.zigbee.device_management" diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua index 354cd3bf9f..f2de6f2d81 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device) local ZLL_DIMMER_BULB_FINGERPRINTS = require "zll-dimmer-bulb.fingerprints" local can_handle = (ZLL_DIMMER_BULB_FINGERPRINTS[device:get_manufacturer()] or {})[device:get_model()] diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua index 9ff7bb0618..ab13464d62 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { ["AduroSmart Eria"] = { ["ZLL-DimmableLight"] = true, diff --git a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua index 2347345f49..d6becf1467 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-dimmer-bulb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua index 3ddd9b8328..c39ee387e3 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(opts, driver, device, zb_rx, ...) local constants = require "st.zigbee.constants" diff --git a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua index ad619e070e..3478b39bd5 100644 --- a/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zll-polling/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local device_lib = require "st.device" local clusters = require "st.zigbee.zcl.clusters" From 778b5a9e5f6309e8eb1dd7cb6c0b44cf5b595df6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:50:43 -0600 Subject: [PATCH 234/449] Matter Smoke Alarm: add smoke-temp-humidity-battery profile (#2513) --- .../matter-sensor/fingerprints.yml | 7 +++++ .../profiles/smoke-temp-humidity-battery.yml | 27 +++++++++++++++++++ .../matter-sensor/src/smoke-co-alarm/init.lua | 1 + 3 files changed, 35 insertions(+) create mode 100644 drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 9a463e7db6..af05f7bafe 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -277,6 +277,13 @@ matterGeneric: deviceTypes: - id: 0x0076 # Smoke CO Alarm deviceProfileName: smoke-co + - id: "matter/smoke-temp-humidity/sensor" + deviceLabel: Matter Smoke/Temp/Humidity Sensor + deviceTypes: + - id: 0x0076 # Smoke CO Alarm + - id: 0x0307 # Humidity Sensor + - id: 0x0302 # Temperature Sensor + deviceProfileName: smoke-temp-humidity-battery - id: "matter/rain/sensor" deviceLabel: Matter Rain Sensor deviceTypes: diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml new file mode 100644 index 0000000000..1f1b61b536 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml @@ -0,0 +1,27 @@ +name: smoke-temp-humidity-battery +components: +- id: main + capabilities: + - id: smokeDetector + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: hardwareFault + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: SmokeDetector +preferences: + - preferenceId: certifiedpreferences.smokeSensorSensitivity + explicit: true + - preferenceId: tempOffset + explicit: true + - preferenceId: humidityOffset + explicit: true \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua b/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua index c5fe00a4be..f622a3a4ba 100644 --- a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua +++ b/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua @@ -64,6 +64,7 @@ local supported_profiles = "co-comeas-colevel-battery", "smoke", "smoke-battery", + "smoke-temp-humidity-battery", "smoke-co-comeas", "smoke-co-comeas-battery", "smoke-co-temp-humidity-comeas", From 445f9777fb45780a870d80a6071fd227f95e82e3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 4 Nov 2025 14:20:03 -0800 Subject: [PATCH 235/449] WWSTCERT-8677 Leviton Decora Smart Wi-Fi (2nd Gen) Scene Controller Switch --- 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 4654f4a84f..ca4f83b3fe 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -641,6 +641,11 @@ matterManufacturer: vendorId: 0x109B productId: 0x1006 deviceProfileName: light-level-motion + - id: "4251/4103" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Scene Controller Switch + vendorId: 0x109B + productId: 0x1007 + deviceProfileName: 3-button #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From b2d9b1c847f6334a65989bc99bdf8cfbcdf32477 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 4 Nov 2025 14:26:30 -0800 Subject: [PATCH 236/449] WWSTCERT-8703 NodOn Zigbee ON/OFF Lighting Relay Switch --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index cb3890125d..236758b30d 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2380,6 +2380,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-1-20 deviceProfileName: basic-switch + - id: "NodOn/SIN-4-2-20" + deviceLabel: Zigbee ON/OFF Lighting Relay Switch + manufacturer: NodOn + model: SIN-4-2-20 + deviceProfileName: switch-level zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From 1dabb8137fd3c4eeb793a7edb8703441afa44511 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 4 Nov 2025 14:49:32 -0800 Subject: [PATCH 237/449] WWSTCERT-8759 OSRAM MATTER CLASSIC A 100W WWSTCERT-8762 SMART MAT A60 RGBW 827 FR E27 --- 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 4654f4a84f..23c1affd54 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -884,6 +884,16 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0AC5 deviceProfileName: light-color-level + - id: "4489/61442" + deviceLabel: OSRAM MATTER CLASSIC A 100W + vendorId: 0x1189 + productId: 0xF002 + deviceProfileName: light-color-level + - id: "4489/2755" + deviceLabel: SMART MAT A60 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC3 + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From fbfbc3adc180f14368f382244d1078cb74149343 Mon Sep 17 00:00:00 2001 From: thinkaName <962679819@qq.com> Date: Wed, 5 Nov 2025 13:51:46 +0800 Subject: [PATCH 238/449] add HOPOsmart window opener A2230011 --- .../zigbee-window-treatment/fingerprints.yml | 5 + .../profiles/window-shade-only.yml | 12 ++ .../src/HOPOsmart/custom_clusters.lua | 30 +++ .../src/HOPOsmart/init.lua | 89 ++++++++ .../zigbee-window-treatment/src/init.lua | 3 +- ...est_zigbee_window_shade_only_HOPOsmart.lua | 198 ++++++++++++++++++ tools/localizations/cn.csv | 1 + 7 files changed, 337 insertions(+), 1 deletion(-) create mode 100755 drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua create mode 100755 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index c677cbce4b..e15dd6f3a2 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -133,6 +133,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-RS-20 deviceProfileName: window-treatment-profile + - id: "HOPOsmart/A2230011" + deviceLabel: HOPOsmart Window Opener A2230011 + manufacturer: HOPOsmart + model: A2230011 + deviceProfileName: window-shade-only zigbeeGeneric: - id: "genericShade" diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml new file mode 100755 index 0000000000..7adffe309e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-shade-only.yml @@ -0,0 +1,12 @@ +name: window-shade-only +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WindowOpener diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua new file mode 100755 index 0000000000..1f2950b2f1 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua @@ -0,0 +1,30 @@ +-- Copyright 2025 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 data_types = require "st.zigbee.data_types" + +local custom_clusters = { + motor = { + id = 0xFCC8, + mfg_specific_code = 0x1235, + attributes = { + state_value = { + id = 0x0000, + value_type = data_types.Uint8, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua new file mode 100755 index 0000000000..5e3f002ec0 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua @@ -0,0 +1,89 @@ +-- Copyright 2025 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 custom_clusters = require "HOPOsmart/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "HOPOsmart", model = "A2230011" } +} + + +local is_zigbee_window_shade = function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function state_value_attr_handler(driver, device, value, zb_rx) + if value.value == 0 then + device:emit_event(capabilities.windowShade.windowShade.open()) + elseif value.value == 1 then + device:emit_event(capabilities.windowShade.windowShade.opening()) + elseif value.value == 2 then + device:emit_event(capabilities.windowShade.windowShade.closed()) + elseif value.value == 3 then + device:emit_event(capabilities.windowShade.windowShade.closing()) + elseif value.value == 4 then + device:emit_event(capabilities.windowShade.windowShade.partially_open()) + end +end + +local function do_refresh(driver, device) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.state_value) +end + +local function added_handler(self, device) + do_refresh(self, device) +end + +local HOPOsmart_handler = { + NAME = "HOPOsmart Device Handler", + supported_capabilities = { + capabilities.refresh + }, + lifecycle_handlers = { + added = added_handler + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + attr = { + [custom_clusters.motor.id] = { + [custom_clusters.motor.attributes.state_value.id] = state_value_attr_handler + } + } + }, + can_handle = is_zigbee_window_shade, +} + +return HOPOsmart_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 246d65f463..3403b3d528 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -57,7 +57,8 @@ local zigbee_window_treatment_driver_template = { require("yoolax"), require("hanssem"), require("screen-innovations"), - require("VIVIDSTORM")}, + require("VIVIDSTORM"), + require("HOPOsmart")}, lifecycle_handlers = { init = init_handler, added = added_handler 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 new file mode 100755 index 0000000000..1e4a8b3a2a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua @@ -0,0 +1,198 @@ +-- Copyright 2025 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 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 data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PRIVATE_CLUSTER_ID = 0xFCC8 +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("window-shade-only.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "HOPOsmart", + model = "A2230011", + server_clusters = {0x0000, 0xFCC8} + } + } + } +) + +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( + "lifecycle - added test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.WindowCovering.server.commands.UpOrOpen(mock_device) } + } + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.DownOrClose(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_coroutine_test( + "Device reported 0 and driver emit windowShade.open", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 0 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.open())) + end +) + +test.register_coroutine_test( + "Device reported 1 and driver emit windowShade.opening", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 1 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.opening())) + end +) + +test.register_coroutine_test( + "Device reported 2 and driver emit windowShade.closed", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 2 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.closed())) + end +) + +test.register_coroutine_test( + "Device reported 3 and driver emit windowShade.closeing", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.closing())) + end +) + +test.register_coroutine_test( + "Device reported 4 and driver emit windowShade.partially_open", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 4 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.windowShade.windowShade.partially_open())) + end +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index d9105f0949..b66d04197a 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -117,3 +117,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H +"HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 From 4a790ec0dbf297bd9c5703a6db95c72e351590e6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:21:41 -0600 Subject: [PATCH 239/449] Matter Switch: Update Directory Names and Copyright Information (#2524) * update Matter Switch file organization * add copyright to embedded clusters --- .../ElectricalEnergyMeasurement/init.lua | 3 ++ .../attributes/CumulativeEnergyImported.lua | 3 ++ .../attributes/PeriodicEnergyImported.lua | 3 ++ .../server/attributes/init.lua | 3 ++ .../types/EnergyMeasurementStruct.lua | 3 ++ .../types/Feature.lua | 3 ++ .../types/init.lua | 3 ++ .../ElectricalPowerMeasurement/init.lua | 3 ++ .../server/attributes/ActivePower.lua | 3 ++ .../server/attributes/init.lua | 3 ++ .../ValveConfigurationAndControl/init.lua | 3 ++ .../server/attributes/CurrentLevel.lua | 3 ++ .../server/attributes/CurrentState.lua | 3 ++ .../server/attributes/init.lua | 3 ++ .../server/commands/Close.lua | 3 ++ .../server/commands/Open.lua | 3 ++ .../server/commands/init.lua | 3 ++ .../types/Feature.lua | 3 ++ .../types/ValveStateEnum.lua | 3 ++ .../types/init.lua | 3 ++ .../SmartThings/matter-switch/src/init.lua | 31 ++++++------------- .../src/sub_drivers/aqara_cube/init.lua | 15 ++------- .../src/sub_drivers/eve_energy/init.lua | 15 ++------- .../sub_drivers/third_reality_mk1/init.lua | 15 ++------- .../attribute_handlers.lua | 22 +++---------- .../capability_handlers.lua | 19 +++--------- .../event_handlers.lua | 19 +++--------- .../{utils => switch_utils}/color_utils.lua | 15 ++------- .../device_configuration.lua | 22 +++---------- .../embedded_cluster_utils.lua | 15 ++------- .../fields.lua} | 15 ++------- .../utils.lua} | 19 +++--------- .../test/test_aqara_climate_sensor_w100.lua | 15 ++------- .../src/test/test_aqara_cube.lua | 3 ++ .../src/test/test_aqara_light_switch_h2.lua | 15 ++------- .../src/test/test_electrical_sensor.lua | 15 ++------- .../src/test/test_eve_energy.lua | 15 ++------- .../test/test_light_illuminance_motion.lua | 15 ++------- .../src/test/test_matter_bridge.lua | 15 ++------- .../src/test/test_matter_button.lua | 3 ++ .../src/test/test_matter_light_fan.lua | 15 ++------- .../src/test/test_matter_multi_button.lua | 3 ++ .../test/test_matter_multi_button_motion.lua | 3 ++ .../test_matter_multi_button_switch_mcd.lua | 3 ++ .../src/test/test_matter_switch.lua | 15 ++------- .../test/test_matter_switch_device_types.lua | 15 ++------- .../src/test/test_matter_water_valve.lua | 15 ++------- .../src/test/test_multi_switch_mcd.lua | 15 ++------- .../test_multi_switch_parent_child_lights.lua | 15 ++------- .../test_multi_switch_parent_child_plugs.lua | 15 ++------- .../src/test/test_third_reality_mk1.lua | 15 ++------- 51 files changed, 146 insertions(+), 361 deletions(-) rename drivers/SmartThings/matter-switch/src/{generic_handlers => switch_handlers}/attribute_handlers.lua (96%) rename drivers/SmartThings/matter-switch/src/{generic_handlers => switch_handlers}/capability_handlers.lua (91%) rename drivers/SmartThings/matter-switch/src/{generic_handlers => switch_handlers}/event_handlers.lua (85%) rename drivers/SmartThings/matter-switch/src/{utils => switch_utils}/color_utils.lua (77%) rename drivers/SmartThings/matter-switch/src/{utils => switch_utils}/device_configuration.lua (93%) rename drivers/SmartThings/matter-switch/src/{utils => switch_utils}/embedded_cluster_utils.lua (79%) rename drivers/SmartThings/matter-switch/src/{utils/switch_fields.lua => switch_utils/fields.lua} (95%) rename drivers/SmartThings/matter-switch/src/{utils/switch_utils.lua => switch_utils/utils.lua} (94%) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index 8c96de0563..0f564673a9 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua index 4e35c264f0..2d41790440 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua index 607faa2161..5daccf48ab 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua index e137f08918..57bc0d1f72 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) 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 950b260227..a4c58a3646 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 @@ -1,3 +1,6 @@ +-- Copyright © 2025 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 EnergyMeasurementStruct = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua index 7b9bc03714..e3db76f49d 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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 = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua index 5241c3b864..ec29b53e05 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua index 222c80090b..c9061e3cc4 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua index 457f6484af..f1696509f5 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua index b1a8d57bde..6de69b94ff 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua index 696c324a81..be2141f09c 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" local ValveConfigurationAndControlServerAttributes = require "embedded_clusters.ValveConfigurationAndControl.server.attributes" local ValveConfigurationAndControlServerCommands = require "embedded_clusters.ValveConfigurationAndControl.server.commands" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua index 807beba485..c77f0f54ac 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua index 514659aa70..033d69e0a8 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/CurrentState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua index dbffea4e8d..ddefc6b205 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/attributes/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local attr_mt = {} attr_mt.__attr_cache = {} attr_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua index baee7688ec..263bf0d849 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Close.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" local Close = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua index 3f84040fc0..fd2f62801a 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/Open.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" local Open = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua index 74c402c3fd..86eb2b9546 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/server/commands/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua index ea3ebeb7c1..bcbc7f0f38 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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 = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua index 8b6da46ad8..e445376bb8 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/ValveStateEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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 ValveStateEnum = {} diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua index 22aa362e99..ae8ebed286 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ValveConfigurationAndControl/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c7ac1f4d4f..2302ffc4dc 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" @@ -18,18 +7,16 @@ local device_lib = require "st.device" local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" -local embedded_cluster_utils = require "utils.embedded_cluster_utils" - -local fields = require "utils.switch_fields" -local switch_utils = require "utils.switch_utils" -local cfg = require "utils.device_configuration" +local cfg = require "switch_utils.device_configuration" local device_cfg = cfg.DeviceCfg local switch_cfg = cfg.SwitchCfg local button_cfg = cfg.ButtonCfg - -local attribute_handlers = require "generic_handlers.attribute_handlers" -local event_handlers = require "generic_handlers.event_handlers" -local capability_handlers = require "generic_handlers.capability_handlers" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local attribute_handlers = require "switch_handlers.attribute_handlers" +local event_handlers = require "switch_handlers.event_handlers" +local capability_handlers = require "switch_handlers.capability_handlers" +local embedded_cluster_utils = require "switch_utils.embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua index 455bac34a4..b6ee56c261 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 04c2d7fc1d..6db532934c 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 ------------------------------------------------------------------------------------- -- Definitions 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 a579929fe0..f3c779906f 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua similarity index 96% rename from drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua rename to drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 73844f2fb7..58310ef56b 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -1,26 +1,14 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local version = require "version" local im = require "st.matter.interaction_model" - local st_utils = require "st.utils" -local fields = require "utils.switch_fields" -local switch_utils = require "utils.switch_utils" -local color_utils = require "utils.color_utils" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local color_utils = require "switch_utils.color_utils" local AttributeHandlers = {} diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua similarity index 91% rename from drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua rename to drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index 05a781bc3d..77927ef1d2 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -1,23 +1,12 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local st_utils = require "st.utils" -local switch_utils = require "utils.switch_utils" -local fields = require "utils.switch_fields" local version = require "version" +local switch_utils = require "switch_utils.utils" +local fields = require "switch_utils.fields" local CapabilityHandlers = {} diff --git a/drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua similarity index 85% rename from drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua rename to drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua index c16a3bd2d5..16177b38f4 100644 --- a/drivers/SmartThings/matter-switch/src/generic_handlers/event_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/event_handlers.lua @@ -1,21 +1,10 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local lua_socket = require "socket" -local fields = require "utils.switch_fields" -local switch_utils = require "utils.switch_utils" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" local EventHandlers = {} diff --git a/drivers/SmartThings/matter-switch/src/utils/color_utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua similarity index 77% rename from drivers/SmartThings/matter-switch/src/utils/color_utils.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua index 410920258a..7e4c231a30 100644 --- a/drivers/SmartThings/matter-switch/src/utils/color_utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/color_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --TODO remove the usage of these color utils once 0.48.x has been distributed -- to all hubs. diff --git a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua similarity index 93% rename from drivers/SmartThings/matter-switch/src/utils/device_configuration.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index 23b5ca6873..626080b147 100644 --- a/drivers/SmartThings/matter-switch/src/utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -1,24 +1,12 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "utils.embedded_cluster_utils" local version = require "version" - -local fields = require "utils.switch_fields" -local switch_utils = require "utils.switch_utils" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" +local embedded_cluster_utils = require "switch_utils.embedded_cluster_utils" -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then diff --git a/drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua similarity index 79% rename from drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua index 29af9feebd..8f52a1488c 100644 --- a/drivers/SmartThings/matter-switch/src/utils/embedded_cluster_utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/embedded_cluster_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.matter.clusters" local utils = require "st.utils" diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua similarity index 95% rename from drivers/SmartThings/matter-switch/src/utils/switch_fields.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index dad46d4c92..f66773d77f 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua similarity index 94% rename from drivers/SmartThings/matter-switch/src/utils/switch_utils.lua rename to drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 2715f2e242..0f7ac435cb 100644 --- a/drivers/SmartThings/matter-switch/src/utils/switch_utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -1,18 +1,7 @@ --- Copyright 2025 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 fields = require "utils.switch_fields" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local fields = require "switch_utils.fields" local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" 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 d2e99b2331..6ed557ed82 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 e641cb88ca..3c377255ab 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua @@ -1,3 +1,6 @@ +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.add_package_capability("cubeAction.yml") test.add_package_capability("cubeFace.yml") 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 da8066f828..52d055ca90 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 06ec130878..920e24c6aa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 5ac4cea964..c3d7faa4eb 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 22fdbb316a..dda93ccd01 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 0d11bc8b5e..1aaef32d37 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 23023b3d02..980bb34aba 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" 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 078b99c7fe..d50d16b8fe 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.generated.zap_clusters" 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 d8b0116101..e1cde750f4 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 @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" 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 0869671622..afa2b5cb6f 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 @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" 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 af7511100e..ab35eff35c 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 @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" 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 8b3bbd8ae2..ba9ae3c921 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 056c103eed..560230cdc7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 16f0ea3103..6129dfd116 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 8680a57740..75e88b5fce 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index cff4c7b60b..0d3cd90854 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 323efea478..cc076fd35e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 31f99f537d..0a72a1316e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" From a512c65cf38fc23422b08363ab3ad82634f6ef91 Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 5 Nov 2025 12:28:30 -0600 Subject: [PATCH 240/449] fix: Relax timeouts for threaded REST responses. --- drivers/SmartThings/philips-hue/src/hue/api.lua | 10 +++++----- .../philips-hue/src/utils/grouped_utils.lua | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/hue/api.lua b/drivers/SmartThings/philips-hue/src/hue/api.lua index dbcc5c4c19..e05fa3d0a0 100644 --- a/drivers/SmartThings/philips-hue/src/hue/api.lua +++ b/drivers/SmartThings/philips-hue/src/hue/api.lua @@ -132,7 +132,7 @@ function PhilipsHueApi.new_bridge_manager(base_url, api_key, socket_builder) true )) local control_tx, control_rx = channel.new() - control_rx:settimeout(30) + control_rx:settimeout(45) local self = setmetatable( { headers = { [APPLICATION_KEY_HEADER] = api_key or "" }, @@ -217,7 +217,7 @@ end ---@return ... local function do_get(instance, path) local reply_tx, reply_rx = channel.new() - reply_rx:settimeout(10) + reply_rx:settimeout(45) local msg = ControlMessageBuilders.Get(path, reply_tx); try_send(instance, msg) local recv, err = reply_rx:receive() @@ -236,7 +236,7 @@ end ---@return ... local function do_put(instance, path, payload) local reply_tx, reply_rx = channel.new() - reply_rx:settimeout(10) + reply_rx:settimeout(45) local msg = ControlMessageBuilders.Put(path, payload, reply_tx); try_send(instance, msg) local recv, err = reply_rx:receive() @@ -255,7 +255,7 @@ end ---@return ... function PhilipsHueApi.get_bridge_info(bridge_ip, socket_builder) local tx, rx = channel.new() - rx:settimeout(10) + rx:settimeout(45) cosock.spawn( function() tx:send(table.pack(process_rest_response(RestClient.one_shot_get("https://" .. bridge_ip .. "/api/config", nil, @@ -278,7 +278,7 @@ end ---@return ... function PhilipsHueApi.request_api_key(bridge_ip, socket_builder) local tx, rx = channel.new() - rx:settimeout(10) + rx:settimeout(45) cosock.spawn( function() local body = json.encode { devicetype = "smartthings_edge_driver#" .. bridge_ip, generateclientkey = true } diff --git a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua index d3fb4813c9..f169381eed 100644 --- a/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua +++ b/drivers/SmartThings/philips-hue/src/utils/grouped_utils.lua @@ -287,7 +287,7 @@ function grouped_utils.queue_group_scan(driver, bridge_device) if queue == nil then local tx, rx = cosock.channel.new() -- Set timeout to 30 seconds to allow for other queued scans to come in. - rx:settimeout(30) + rx:settimeout(45) cosock.spawn(function() while true do -- The goal here is to timeout on the receive. If we receive a message then another request From 3fc413e3b4cdccc440a2dcd8e2aa2fb3115ee22c Mon Sep 17 00:00:00 2001 From: Doug Stephen Date: Wed, 5 Nov 2025 12:33:55 -0600 Subject: [PATCH 241/449] fix: Multi-button service mapping improvements. For multi-button devices, we were waiting to populate the mappings to associate the multiple button service resources to the single device record from the API responses we perform during the `init` handler. This could lead to inconsistent state or failure to maintain mappings in situations where those API calls failed for any reason. We still need to make those API calls during `init` to establish the current state of the device, but we now populate the mappings from cached data (if it exists) instead of relying on those initial API calls to resolve properly. --- .../handlers/lifecycle_handlers/button.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua index 6c1d41dbb8..d10ad5ee65 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/lifecycle_handlers/button.lua @@ -119,6 +119,25 @@ function ButtonLifecycleHandlers.init(driver, device) if not hue_id_to_device[device_button_resource_id] then hue_id_to_device[device_button_resource_id] = device end + + local maybe_idx_map = device:get_field(Fields.BUTTON_INDEX_MAP) + local svc_rids_for_device = driver.services_for_device_rid[hue_device_id] or {} + + if not svc_rids_for_device[device_button_resource_id] then + svc_rids_for_device[device_button_resource_id] = HueDeviceTypes.BUTTON + end + + for resource_id, _ in pairs(maybe_idx_map or {}) do + if not hue_id_to_device[resource_id] then + hue_id_to_device[resource_id] = device + end + + if not svc_rids_for_device[resource_id] then + svc_rids_for_device[resource_id] = HueDeviceTypes.BUTTON + end + end + driver.services_for_device_rid[hue_device_id] = svc_rids_for_device + local button_info, err button_info = Discovery.device_state_disco_cache[device_button_resource_id] if not button_info then From d54d106b2e1b718a2d1f108aeea2c8f18aedfd17 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:36:57 -0600 Subject: [PATCH 242/449] remove smokeSensitivity preference from profile (#2532) --- .../matter-sensor/profiles/smoke-temp-humidity-battery.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml index 1f1b61b536..cadbe9eb8a 100644 --- a/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml +++ b/drivers/SmartThings/matter-sensor/profiles/smoke-temp-humidity-battery.yml @@ -19,8 +19,6 @@ components: categories: - name: SmokeDetector preferences: - - preferenceId: certifiedpreferences.smokeSensorSensitivity - explicit: true - preferenceId: tempOffset explicit: true - preferenceId: humidityOffset From e8d0bd384e33aaa1434f61d01a650e12c03e48cf Mon Sep 17 00:00:00 2001 From: Gene Harvey Date: Thu, 6 Nov 2025 11:03:16 -0600 Subject: [PATCH 243/449] Stop accounting for leakage of variables between tests Due to changes in the v16 API, certain variable values are no longer leaked between tests. This commit accounts for that. --- .../test_zigbee_window_treatment_hanssem.lua | 7 ++++++ .../src/test/test_tuya_curtain.lua | 10 ++++----- .../tuya-zigbee/src/test/test_tuya_switch.lua | 22 +++++++++---------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index 23814eff28..2e511c99bb 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -88,6 +88,7 @@ end test.register_coroutine_test( "Device Added ", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( @@ -106,6 +107,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Open handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x00") @@ -147,6 +149,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Close handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -189,6 +192,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Pause handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -243,6 +247,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Set Level handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -285,6 +290,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Preset position handler", function() + SeqNum = 0 test.socket.capability:__queue_receive( { mock_device.id, @@ -336,6 +342,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Information changed : Reverse", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.zigbee:__queue_receive({ mock_device.id, 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 7a0ea0b215..ca09608347 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -145,7 +145,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 2) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 0) } }, { channel = "capability", @@ -166,7 +166,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 3) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 0) } }, { channel = "capability", @@ -187,7 +187,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 4) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 0) } } } ) @@ -203,7 +203,7 @@ test.register_message_test( { channel = "zigbee", 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', 5) } + 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) } } } ) @@ -219,7 +219,7 @@ test.register_message_test( { channel = "zigbee", 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', 6) } + 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) } } } ) 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 4da9d411d7..97537c36ad 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua @@ -252,7 +252,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 1) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -268,7 +268,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 2) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -284,7 +284,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 3) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -300,7 +300,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 4) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -316,7 +316,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 5) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -332,7 +332,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 6) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -348,7 +348,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 7) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -364,7 +364,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 8) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -380,7 +380,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 9) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -396,7 +396,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 10) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -412,7 +412,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 11) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) From fd370c2bcc76d6a30fa3e2689c25bb96f34b30ea Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:57:00 -0600 Subject: [PATCH 244/449] Fix test cases using EnergyMeasurementStruct (#2470) Two fields were added to the type definition for EnergyMeasurementStruct and must be added to tests using this struct to fix test case failures. --- .../matter-energy/src/test/test_battery_storage.lua | 8 ++++---- .../matter-energy/src/test/test_solar_power.lua | 8 ++++---- .../src/test/test_aqara_light_switch_h2.lua | 6 ++++++ .../src/test/test_electrical_sensor.lua | 8 ++++++++ .../src/test/test_matter_heat_pump.lua | 12 ++++++------ .../src/test/test_matter_water_heater.lua | 8 ++++---- 6 files changed, 32 insertions(+), 18 deletions(-) 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 e2c3cbd44c..5b5734acf5 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -196,7 +196,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + 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 test.socket.capability:__expect_send( mock_device:generate_test_message("importedEnergy", @@ -219,7 +219,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --400Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --400Wh test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", @@ -245,7 +245,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 200000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --200Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 200000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --200Wh test.socket.capability:__expect_send( mock_device:generate_test_message("importedEnergy", @@ -266,7 +266,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, BATTERY_STORAGE_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --400Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 400000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --400Wh test.socket.capability:__expect_send( mock_device:generate_test_message("exportedEnergy", 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 8c5e4c6ff8..c7446af44b 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -133,7 +133,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + 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 test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -156,7 +156,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_TWO, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --150Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 150000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --150Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -178,7 +178,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyExported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + 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 test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -190,7 +190,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, SOLAR_POWER_EP_ONE, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) --100Wh + 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 ) 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 52d055ca90..ec91ddba9f 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 @@ -119,6 +119,8 @@ local cumulative_report_val_19 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_29 = { @@ -127,6 +129,8 @@ local cumulative_report_val_29 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_39 = { @@ -135,6 +139,8 @@ local cumulative_report_val_39 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local function configure_buttons() diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index 920e24c6aa..b9db2a92b2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -110,6 +110,8 @@ local cumulative_report_val_19 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_29 = { @@ -118,6 +120,8 @@ local cumulative_report_val_29 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local cumulative_report_val_39 = { @@ -126,6 +130,8 @@ local cumulative_report_val_39 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local periodic_report_val_23 = { @@ -134,6 +140,8 @@ local periodic_report_val_23 = { end_timestamp = 0, start_systime = 0, end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 } local function test_init() 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 b7d4b3a24a..ba097d0451 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 @@ -633,7 +633,7 @@ test.register_message_test( clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) } }, { @@ -651,7 +651,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -675,7 +675,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -698,7 +698,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -722,12 +722,12 @@ test.register_coroutine_test( -- do not expect energyMeter event for this report. test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 800, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 800, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh -- do not expect a powerConsumptionReport to be emitted test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, HEAT_PUMP_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 50Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 50Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", 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 802f323181..cbd6464a48 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 @@ -249,7 +249,7 @@ test.register_message_test( clusters.ElectricalEnergyMeasurement.attributes .CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 15000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) } }, { @@ -267,7 +267,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 20Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 20000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 20Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -288,7 +288,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 30000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", @@ -303,7 +303,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive({ mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(mock_device, ELECTRICAL_SENSOR_EP, - clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0 })) }) -- 30Wh + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 50000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) -- 30Wh test.socket.capability:__expect_send( mock_device:generate_test_message("main", From e905a119c72094e8aa59b94ea1308bc0b3588d45 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 7 Nov 2025 10:53:21 -0800 Subject: [PATCH 245/449] WWSTCERT-8788 NodOn Zigbee Multifunction Relay Switch with Metering --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 236758b30d..a016658b9e 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2385,6 +2385,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-2-20 deviceProfileName: switch-level + - id: "NodOn/SIN-4-1-21" + deviceLabel: Zigbee Multifunction Relay Switch with Metering + manufacturer: NodOn + model: SIN-4-1-21 + deviceProfileName: switch-power-energy zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From a0bb019957752dfb4043f5445bd043f6343b5717 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 7 Nov 2025 11:04:18 -0800 Subject: [PATCH 246/449] WWSTCERT-8799 Heiman Smart Humidity&Temperature Sensor --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index af05f7bafe..c1e2f2dc04 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -74,6 +74,11 @@ matterManufacturer: vendorId: 0x120B productId: 0x1008 deviceProfileName: leak-battery + - id: "4619/4192" + deviceLabel: Smart Humidity&Temperature Sensor + vendorId: 0x120B + productId: 0x1060 + deviceProfileName: temperature-humidity-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor From 588bd128a1b0cdd51caf04fad637e523cbfc658d Mon Sep 17 00:00:00 2001 From: thinkaName <962679819@qq.com> Date: Mon, 10 Nov 2025 10:38:16 +0800 Subject: [PATCH 247/449] add yanmi Y-K003-001 switch --- .../zigbee-switch/fingerprints.yml | 5 + .../multi-switch-no-master/fingerprints.lua | 3 +- .../src/test/test_yanmi_switch.lua | 409 ++++++++++++++++++ tools/localizations/cn.csv | 1 + 4 files changed, 417 insertions(+), 1 deletion(-) create mode 100755 drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index a016658b9e..d5f2e4d8df 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2390,6 +2390,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-1-21 deviceProfileName: switch-power-energy + - id: "JNL/Y-K003-001" + deviceLabel: Yanmi Switch (3 Way) 1 + manufacturer: JNL + model: Y-K003-001 + deviceProfileName: basic-switch zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch diff --git a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua index ca54566d8d..48dd5ac246 100644 --- a/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/multi-switch-no-master/fingerprints.lua @@ -42,5 +42,6 @@ return { { model = "E220-KR3N0Z0-HA", children = 2 }, { model = "E220-KR4N0Z0-HA", children = 3 }, { model = "E220-KR5N0Z0-HA", children = 4 }, - { model = "E220-KR6N0Z0-HA", children = 5 } + { model = "E220-KR6N0Z0-HA", children = 5 }, + { mfr = "JNL", model = "Y-K003-001", children = 2 } } diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua new file mode 100755 index 0000000000..61ad14e75d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua @@ -0,0 +1,409 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local OnOff = clusters.OnOff + +local common_switch_profile_def = t_utils.get_profile_definition("basic-switch.yml") + + +local mock_parent_device = test.mock_device.build_test_zigbee_device( + { + profile = common_switch_profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "JNL", + model = "Y-K003-001", + server_clusters = { 0003,0004,0005,0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + +-- Switch 2 (Common Switch) +local mock_first_child = test.mock_device.build_test_child_device( + { + profile = common_switch_profile_def, + device_network_id = string.format("%04X:%02X", mock_parent_device:get_short_address(), 2), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%02X", 2) + } +) + +-- Switch 3 (Common Switch) +local mock_second_child = test.mock_device.build_test_child_device( + { + profile = common_switch_profile_def, + device_network_id = string.format("%04X:%02X", mock_parent_device:get_short_address(), 3), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = string.format("%02X", 3) + } +) + + + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_first_child) + test.mock_device.add_test_device(mock_second_child) +end + +test.set_test_init_function(test_init) + +-- 4 Common Switch Tests +test.register_message_test( + "Reported on off status should be handled by parent device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_parent_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_parent_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by first child device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_first_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_first_child:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by Second child device: on", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_second_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + true) :from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_second_child:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + + +test.register_message_test( + "reported on off status should be handled by parent device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_parent_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_parent_device:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by first child device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_first_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_first_child:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.register_message_test( + "Reported on off status should be handled by Second child device: off", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "zigbee", + direction = "receive", + message = { mock_second_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, + false) :from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_second_child:generate_test_message("main", capabilities.switch.switch.off()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + + +test.register_message_test( + "Capability on command switch on should be handled : parent device", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_parent_device.id, "init" } + }, + { + channel = "capability", + direction = "receive", + message = { mock_parent_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : first child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_first_child.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability on command switch on should be handled : second child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_second_child.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.On(mock_parent_device):to_endpoint(0x03) } + } + } +) + + + + +test.register_message_test( + "Capability off command switch off should be handled : parent device", + { + { + channel = "capability", + direction = "receive", + message = { mock_parent_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_parent_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x01) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : first child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_first_child.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_first_child.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Capability off command switch off should be handled : second child device", + { + { + channel = "capability", + direction = "receive", + message = { mock_second_child.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_second_child.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_parent_device.id, OnOff.server.commands.Off(mock_parent_device):to_endpoint(0x03) } + } + } +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index b66d04197a..44f7589bce 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -118,3 +118,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H "HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 +"Yanmi Switch (3 Way)",岩米三位智能开关面板 From 737773e665a0dfb097501e8d0b23234b88360c1d Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:15:24 -0600 Subject: [PATCH 248/449] Matter Sensor: Re-organize the directory structure (#2521) --- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../server/attributes/LevelValue.lua | 41 -- .../server/attributes/MeasuredValue.lua | 56 -- .../server/attributes/MeasurementUnit.lua | 45 -- .../src/air-quality-sensor/init.lua | 677 ------------------ .../AirQuality/init.lua | 7 +- .../server/attributes/AcceptedCommandList.lua | 3 + .../server/attributes/AirQuality.lua | 5 +- .../server/attributes/AttributeList.lua | 3 + .../server/attributes/EventList.lua | 3 + .../AirQuality/server/attributes/init.lua | 5 +- .../AirQuality/types/AirQualityEnum.lua | 3 + .../AirQuality/types/Feature.lua | 3 + .../AirQuality/types/init.lua | 5 +- .../BooleanStateConfiguration/init.lua | 4 +- .../attributes/CurrentSensitivityLevel.lua | 3 + .../attributes/DefaultSensitivityLevel.lua | 3 + .../server/attributes/SensorFault.lua | 5 +- .../attributes/SupportedSensitivityLevels.lua | 3 + .../server/attributes/init.lua | 5 +- .../types/Feature.lua | 3 + .../types/SensorFaultBitmap.lua | 3 + .../BooleanStateConfiguration/types/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 5 +- .../server/attributes/MeasuredValue.lua | 3 + .../server/attributes/MeasurementUnit.lua | 5 +- .../server/attributes/init.lua | 5 +- .../types/Feature.lua | 3 + .../types/LevelValueEnum.lua | 3 + .../types/MeasurementUnitEnum.lua | 3 + .../ConcentrationMeasurement/types/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../OzoneConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../Pm10ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../Pm1ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../Pm25ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../PressureMeasurement/init.lua | 6 +- .../server/attributes/AcceptedCommandList.lua | 0 .../server/attributes/AttributeList.lua | 0 .../server/attributes/EventList.lua | 0 .../server/attributes/MaxMeasuredValue.lua | 0 .../server/attributes/MaxScaledValue.lua | 0 .../server/attributes/MeasuredValue.lua | 0 .../server/attributes/MinMeasuredValue.lua | 0 .../server/attributes/MinScaledValue.lua | 0 .../server/attributes/Scale.lua | 0 .../server/attributes/ScaledTolerance.lua | 0 .../server/attributes/ScaledValue.lua | 0 .../server/attributes/Tolerance.lua | 0 .../server/attributes/init.lua | 2 +- .../server/commands/init.lua | 2 +- .../server/events/init.lua | 2 +- .../PressureMeasurement/types/Feature.lua | 0 .../PressureMeasurement/types/init.lua | 2 +- .../RadonConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../SmokeCoAlarm/init.lua | 9 +- .../server/attributes/BatteryAlert.lua | 5 +- .../server/attributes/COState.lua | 5 +- .../server/attributes/HardwareFaultAlert.lua | 3 + .../attributes/SmokeSensitivityLevel.lua | 5 +- .../server/attributes/SmokeState.lua | 5 +- .../server/attributes/TestInProgress.lua | 3 + .../SmokeCoAlarm/server/attributes/init.lua | 5 +- .../server/commands/SelfTestRequest.lua | 3 + .../SmokeCoAlarm/server/commands/init.lua | 5 +- .../SmokeCoAlarm/types/AlarmStateEnum.lua | 3 + .../SmokeCoAlarm/types/Feature.lua | 3 + .../SmokeCoAlarm/types/SensitivityEnum.lua | 3 + .../SmokeCoAlarm/types/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 ++ .../server/attributes/MeasuredValue.lua | 59 ++ .../server/attributes/MeasurementUnit.lua | 48 ++ .../server/attributes/init.lua | 5 +- .../SmartThings/matter-sensor/src/init.lua | 626 ++++------------ .../sensor_handlers/attribute_handlers.lua | 204 ++++++ .../src/sensor_utils/device_configuration.lua | 124 ++++ .../embedded_cluster_utils.lua} | 29 +- .../matter-sensor/src/sensor_utils/fields.lua | 50 ++ .../matter-sensor/src/sensor_utils/utils.lua | 24 + .../device_configuration.lua | 88 +++ .../sub_drivers/air_quality_sensor/fields.lua | 178 +++++ .../sub_drivers/air_quality_sensor/init.lua | 264 +++++++ .../legacy_device_configuration.lua | 121 ++++ .../bosch_button_contact}/init.lua | 19 +- .../smoke_co_alarm}/init.lua | 184 ++--- .../test/test_matter_air_quality_sensor.lua | 43 +- ...test_matter_air_quality_sensor_modular.lua | 15 +- .../test/test_matter_bosch_button_contact.lua | 15 +- .../src/test/test_matter_flow_sensor.lua | 15 +- .../test/test_matter_freeze_leak_sensor.lua | 21 +- .../src/test/test_matter_pressure_sensor.lua | 33 +- .../src/test/test_matter_rain_sensor.lua | 20 +- .../src/test/test_matter_sensor.lua | 15 +- .../src/test/test_matter_sensor_battery.lua | 15 +- .../test/test_matter_sensor_featuremap.lua | 15 +- .../src/test/test_matter_sensor_rpc.lua | 15 +- .../src/test/test_matter_smoke_co_alarm.lua | 20 +- .../test_matter_smoke_co_alarm_battery.lua | 24 +- 153 files changed, 2567 insertions(+), 2427 deletions(-) delete mode 100644 drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/init.lua (86%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/server/attributes/AcceptedCommandList.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/server/attributes/AirQuality.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/server/attributes/AttributeList.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/server/attributes/EventList.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/server/attributes/init.lua (75%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/types/AirQualityEnum.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/types/Feature.lua (96%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/AirQuality/types/init.lua (60%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/init.lua (92%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua (95%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/server/attributes/SensorFault.lua (87%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/types/Feature.lua (97%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/types/SensorFaultBitmap.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/BooleanStateConfiguration/types/init.lua (62%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/CarbonDioxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-sensor/src/{NitrogenDioxideConcentrationMeasurement => embedded_clusters/CarbonDioxideConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-sensor/src/{CarbonMonoxideConcentrationMeasurement => embedded_clusters/CarbonDioxideConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-sensor/src/{FormaldehydeConcentrationMeasurement => embedded_clusters/CarbonDioxideConcentrationMeasurement}/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-sensor/src/{FormaldehydeConcentrationMeasurement => embedded_clusters/CarbonMonoxideConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-sensor/src/{NitrogenDioxideConcentrationMeasurement => embedded_clusters/CarbonMonoxideConcentrationMeasurement}/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/init.lua (92%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/LevelValue.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/MeasuredValue.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/types/Feature.lua (98%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/types/LevelValueEnum.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/types/MeasurementUnitEnum.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/ConcentrationMeasurement/types/init.lua (62%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-sensor/src/{CarbonDioxideConcentrationMeasurement => embedded_clusters/FormaldehydeConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-sensor/src/{CarbonDioxideConcentrationMeasurement => embedded_clusters/FormaldehydeConcentrationMeasurement}/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-sensor/src/{CarbonDioxideConcentrationMeasurement => embedded_clusters/NitrogenDioxideConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-sensor/src/{CarbonMonoxideConcentrationMeasurement => embedded_clusters/NitrogenDioxideConcentrationMeasurement}/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/OzoneConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/OzoneConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm10ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm10ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm1ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm1ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm25ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/Pm25ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/init.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/AcceptedCommandList.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/AttributeList.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/EventList.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/MaxMeasuredValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/MaxScaledValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/MeasuredValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/MinMeasuredValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/MinScaledValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/Scale.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/ScaledTolerance.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/ScaledValue.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/Tolerance.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/attributes/init.lua (95%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/commands/init.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/server/events/init.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/types/Feature.lua (100%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/PressureMeasurement/types/init.lua (91%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/RadonConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/RadonConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/init.lua (90%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/BatteryAlert.lua (89%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/COState.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua (94%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua (91%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/SmokeState.lua (88%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/TestInProgress.lua (93%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/attributes/init.lua (75%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/commands/SelfTestRequest.lua (96%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/server/commands/init.lua (76%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/types/AlarmStateEnum.lua (92%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/types/Feature.lua (95%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/types/SensitivityEnum.lua (91%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/SmokeCoAlarm/types/init.lua (60%) rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua (89%) create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-sensor/src/{ => embedded_clusters}/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua (76%) create mode 100644 drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua rename drivers/SmartThings/matter-sensor/src/{embedded-cluster-utils.lua => sensor_utils/embedded_cluster_utils.lua} (70%) create mode 100644 drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua rename drivers/SmartThings/matter-sensor/src/{bosch-button-contact => sub_drivers/bosch_button_contact}/init.lua (91%) rename drivers/SmartThings/matter-sensor/src/{smoke-co-alarm => sub_drivers/smoke_co_alarm}/init.lua (63%) diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua b/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua deleted file mode 100644 index 37a03d1580..0000000000 --- a/drivers/SmartThings/matter-sensor/src/air-quality-sensor/init.lua +++ /dev/null @@ -1,677 +0,0 @@ --- Copyright 2024 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 clusters = require "st.matter.clusters" -local utils = require "st.utils" -local embedded_cluster_utils = require "embedded-cluster-utils" - -local log = require "log" -local AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C - -local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" - - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" -end - -local function is_matter_air_quality_sensor(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then - return true - end - end - end - - return false - end - -local subscribed_attributes = { - [capabilities.airQualityHealthConcern.ID] = { - clusters.AirQuality.attributes.AirQuality - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.formaldehydeHealthConcern.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.radonMeasurement.ID] = { - clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, - clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.tvocMeasurement.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue - } -} - -local units_required = { - clusters.CarbonMonoxideConcentrationMeasurement, - clusters.CarbonDioxideConcentrationMeasurement, - clusters.NitrogenDioxideConcentrationMeasurement, - clusters.OzoneConcentrationMeasurement, - clusters.FormaldehydeConcentrationMeasurement, - clusters.Pm1ConcentrationMeasurement, - clusters.Pm25ConcentrationMeasurement, - clusters.Pm10ConcentrationMeasurement, - clusters.RadonConcentrationMeasurement, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement -} - -local tbl_contains = function(t, val) - for _, v in pairs(t) do - if v == val then - return true - end - end - return false -end - -local supported_profiles = -{ - "aqs", - "aqs-temp-humidity-all-level-all-meas", - "aqs-temp-humidity-all-level", - "aqs-temp-humidity-all-meas", - "aqs-temp-humidity-co2-pm25-tvoc-meas", - "aqs-temp-humidity-co2-pm1-pm25-pm10-meas", - "aqs-temp-humidity-tvoc-level-pm25-meas", - "aqs-temp-humidity-tvoc-meas", -} - -local CONCENTRATION_MEASUREMENT_MAP = { - [capabilities.carbonMonoxideMeasurement] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, "N/A"}, - [capabilities.carbonMonoxideHealthConcern] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues}, - [capabilities.carbonDioxideMeasurement] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, "N/A"}, - [capabilities.carbonDioxideHealthConcern] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues}, - [capabilities.nitrogenDioxideMeasurement] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, "N/A"}, - [capabilities.nitrogenDioxideHealthConcern] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues}, - [capabilities.ozoneMeasurement] = {"-ozone", clusters.OzoneConcentrationMeasurement, "N/A"}, - [capabilities.ozoneHealthConcern] = {"-ozone", clusters.OzoneConcentrationMeasurement, capabilities.ozoneHealthConcern.supportedOzoneValues}, - [capabilities.formaldehydeMeasurement] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, "N/A"}, - [capabilities.formaldehydeHealthConcern] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues}, - [capabilities.veryFineDustSensor] = {"-pm1", clusters.Pm1ConcentrationMeasurement, "N/A"}, - [capabilities.veryFineDustHealthConcern] = {"-pm1", clusters.Pm1ConcentrationMeasurement, capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues}, - [capabilities.fineDustSensor] = {"-pm25", clusters.Pm25ConcentrationMeasurement, "N/A"}, - [capabilities.fineDustHealthConcern] = {"-pm25", clusters.Pm25ConcentrationMeasurement, capabilities.fineDustHealthConcern.supportedFineDustValues}, - [capabilities.dustSensor] = {"-pm10", clusters.Pm10ConcentrationMeasurement, "N/A"}, - [capabilities.dustHealthConcern] = {"-pm10", clusters.Pm10ConcentrationMeasurement, capabilities.dustHealthConcern.supportedDustValues}, - [capabilities.radonMeasurement] = {"-radon", clusters.RadonConcentrationMeasurement, "N/A"}, - [capabilities.radonHealthConcern] = {"-radon", clusters.RadonConcentrationMeasurement, capabilities.radonHealthConcern.supportedRadonValues}, - [capabilities.tvocMeasurement] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, "N/A"}, - [capabilities.tvocHealthConcern] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, capabilities.tvocHealthConcern.supportedTvocValues}, -} - - -local CONCENTRATION_MEASUREMENT_PROFILE_ORDERING = { - capabilities.carbonMonoxideMeasurement, - capabilities.carbonMonoxideHealthConcern, - capabilities.carbonDioxideMeasurement, - capabilities.carbonDioxideHealthConcern, - capabilities.nitrogenDioxideMeasurement, - capabilities.nitrogenDioxideHealthConcern, - capabilities.ozoneMeasurement, - capabilities.ozoneHealthConcern, - capabilities.formaldehydeMeasurement, - capabilities.formaldehydeHealthConcern, - capabilities.veryFineDustSensor, - capabilities.veryFineDustHealthConcern, - capabilities.fineDustSensor, - capabilities.fineDustHealthConcern, - capabilities.dustSensor, - capabilities.dustHealthConcern, - capabilities.radonMeasurement, - capabilities.radonHealthConcern, - capabilities.tvocMeasurement, - capabilities.tvocHealthConcern, -} - -local function set_supported_health_concern_values(device, setter_function, cluster, cluster_ep) - -- read_datatype_value works since all the healthConcern capabilities' datatypes are equivalent to the one in airQualityHealthConcern - local read_datatype_value = capabilities.airQualityHealthConcern.airQualityHealthConcern - local supported_values = {read_datatype_value.unknown.NAME, read_datatype_value.good.NAME, read_datatype_value.unhealthy.NAME} - if cluster == clusters.AirQuality then - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.FAIR }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MODERATE }) > 0 then - table.insert(supported_values, 4, read_datatype_value.slightlyUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.VERY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.veryUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.EXTREMELY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - else -- ConcentrationMeasurement clusters - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - end - device:emit_event_for_endpoint(cluster_ep, setter_function(supported_values, { visibility = { displayed = false }})) -end - -local function create_level_measurement_profile(device) - local meas_name, level_name = "", "" - for _, cap in ipairs(CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do - local cap_id = cap.ID - local cluster = CONCENTRATION_MEASUREMENT_MAP[cap][2] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - level_name = level_name .. CONCENTRATION_MEASUREMENT_MAP[cap][1] - set_supported_health_concern_values(device, CONCENTRATION_MEASUREMENT_MAP[cap][3], cluster, attr_eps[1]) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - meas_name = meas_name .. CONCENTRATION_MEASUREMENT_MAP[cap][1] - end - end - end - return meas_name, level_name -end - -local function supported_level_measurements(device) - local measurement_caps, level_caps = {}, {} - for _, cap in ipairs(CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do - local cap_id = cap.ID - local cluster = CONCENTRATION_MEASUREMENT_MAP[cap][2] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - table.insert(level_caps, cap_id) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - table.insert(measurement_caps, cap_id) - end - end - end - return measurement_caps, level_caps -end - -local function match_profile_switch(driver, device) - 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 profile_name = "aqs" - local aq_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - set_supported_health_concern_values(device, capabilities.airQualityHealthConcern.supportedAirQualityValues, clusters.AirQuality, aq_eps[1]) - - if #temp_eps > 0 then - profile_name = profile_name .. "-temp" - end - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - local meas_name, level_name = create_level_measurement_profile(device) - - -- If all endpoints are supported, use '-all' in the profile name so that it - -- remains under the profile name character limit - if level_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then - level_name = "-all" - end - if level_name ~= "" then - profile_name = profile_name .. level_name .. "-level" - end - - -- If all endpoints are supported, use '-all' in the profile name so that it - -- remains under the profile name character limit - if meas_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then - meas_name = "-all" - end - if meas_name ~= "" then - profile_name = profile_name .. meas_name .. "-meas" - end - - if not tbl_contains(supported_profiles, profile_name) then - device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s", profile_name)) - - local function meas_find(sub_name) - return string.match(meas_name, sub_name) ~= nil - end - - -- try to best match to existing profiles - -- these checks, meas_find("co%-") and meas_find("co$"), match the string to co and NOT co2. - if meas_find("co%-") or meas_find("co$") or meas_find("no2") or meas_find("ozone") or meas_find("ch2o") or - meas_find("pm1") or meas_find("pm10") or meas_find("radon") then - profile_name = "aqs-temp-humidity-all-meas" - elseif #humidity_eps > 0 or #temp_eps > 0 or meas_find("co2") or meas_find("pm25") or meas_find("tvoc") then - profile_name = "aqs-temp-humidity-co2-pm25-tvoc-meas" - else - -- device only supports air quality at this point - profile_name = "aqs" - end - 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 - -local function supports_capability_by_id_modular(device, capability, component) - if not device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - -local function match_modular_profile(driver, device) - 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 - - if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - local measurement_caps, level_caps = supported_level_measurements(device) - - 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}) - - -- 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 - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - 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) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function do_configure(driver, device) - -- we have to read the unit before reports of values will do anything - for _, cluster in ipairs(units_required) do - device:send(cluster.attributes.MeasurementUnit:read(device)) - end - if version.api >= 14 and version.rpc >= 8 then - match_modular_profile(driver, device) - else - match_profile_switch(driver, device) - end -end - -local function driver_switched(driver, device) - -- we have to read the unit before reports of values will do anything - for _, cluster in ipairs(units_required) do - device:send(cluster.attributes.MeasurementUnit:read(device)) - end - if version.api >= 14 and version.rpc >= 8 then - match_modular_profile(driver, device) - else - match_profile_switch(driver, device) - end -end - -local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() -end - -local function store_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local units = { - PPM = 0, - PPB = 1, - PPT = 2, - MGM3 = 3, - UGM3 = 4, - NGM3 = 5, - PM3 = 6, - BQM3 = 7, - PCIL = 0xFF -- not in matter spec -} - -local unit_strings = { - [units.PPM] = "ppm", - [units.PPB] = "ppb", - [units.PPT] = "ppt", - [units.MGM3] = "mg/m^3", - [units.NGM3] = "ng/m^3", - [units.UGM3] = "μg/m^3", - [units.BQM3] = "Bq/m^3", - [units.PCIL] = "pCi/L" -} - -local unit_default = { - [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, - [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, - [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, - [capabilities.ozoneMeasurement.NAME] = units.PPM, - [capabilities.formaldehydeMeasurement.NAME] = units.PPM, - [capabilities.veryFineDustSensor.NAME] = units.UGM3, - [capabilities.fineDustSensor.NAME] = units.UGM3, - [capabilities.dustSensor.NAME] = units.UGM3, - [capabilities.radonMeasurement.NAME] = units.BQM3, - [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb -} - --- All ConcentrationMeasurement clusters inherit from the same base cluster definitions, --- so CarbonMonoxideConcentrationMeasurement is used below but the same enum types exist --- in all ConcentrationMeasurement clusters -local level_strings = { - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", -} - -local conversion_tables = { - [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end - }, - [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end - }, - [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end - }, - [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end - }, - [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end - }, - [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end - }, - [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - } -} - -local function unit_conversion(value, from_unit, to_unit) - local conversion_function = conversion_tables[from_unit][to_unit] - if conversion_function == nil then - log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", unit_strings[from_unit], unit_strings[to_unit])) - return 1 - end - - if value == nil then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return 1 - end - return conversion_function(value) -end - -local function measurementHandlerFactory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if reporting_unit == nil then - reporting_unit = unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - if reporting_unit then - local value = unit_conversion(ib.data.value, reporting_unit, target_unit) - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = 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 = unit_strings[target_unit]})) - end - end - end -end - -local function levelHandlerFactory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(level_strings[ib.data.value])) - end -end - --- Matter Handlers -- -local function air_quality_attr_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -end - -local function pressure_attr_handler(driver, device, ib, response) - local pressure = utils.round(ib.data.value / 10.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) -end - -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - --re-up subscription with new capabilities using the modular supports_capability override - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() - end -end - -local matter_air_quality_sensor_handler = { - NAME = "matter-air-quality-sensor", - lifecycle_handlers = { - init = device_init, - doConfigure = do_configure, - infoChanged = info_changed, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = air_quality_attr_handler, - }, - [clusters.PressureMeasurement.ID] = { - [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_attr_handler - }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), - }, - [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), - }, - [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) - }, - [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.ozoneHealthConcern.ozoneHealthConcern) - }, - [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), - }, - [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), - }, - [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.fineDustHealthConcern.fineDustHealthConcern), - }, - [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.dustHealthConcern.dustHealthConcern), - }, - [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.radonHealthConcern.radonHealthConcern) - }, - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.tvocHealthConcern.tvocHealthConcern) - } - } - }, - subscribed_attributes = subscribed_attributes, - can_handle = is_matter_air_quality_sensor -} - -return matter_air_quality_sensor_handler diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua similarity index 86% rename from drivers/SmartThings/matter-sensor/src/AirQuality/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua index 78e77fce51..4f6d19cc9f 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local AirQualityServerAttributes = require "AirQuality.server.attributes" -local AirQualityTypes = require "AirQuality.types" +local AirQualityServerAttributes = require "embedded_clusters.AirQuality.server.attributes" +local AirQualityTypes = require "embedded_clusters.AirQuality.types" local AirQuality = {} diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua index 6a8d95df1d..a1e56c6597 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AcceptedCommandList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua index f92f43543a..1beb6218f9 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AirQuality.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local AirQuality = { ID = 0x0000, NAME = "AirQuality", - base_type = require "AirQuality.types.AirQualityEnum", + base_type = require "embedded_clusters.AirQuality.types.AirQualityEnum", } function AirQuality:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua index 93e96817e6..238b50ade3 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua index 69155cd7ca..719f17a231 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/EventList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/EventList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua index aef3b476a9..50295b081a 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("AirQuality.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.AirQuality.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua index 317a42dc9b..c2c255614a 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/AirQualityEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua index 86b90ce627..906a09a2bb 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/Feature.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua similarity index 60% rename from drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua index 88a2b861b7..b77d67de82 100644 --- a/drivers/SmartThings/matter-sensor/src/AirQuality/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/AirQuality/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("AirQuality.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.AirQuality.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua similarity index 92% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua index f5258619fa..c8e754419b 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/init.lua @@ -1,6 +1,6 @@ local cluster_base = require "st.matter.cluster_base" -local BooleanStateConfigurationServerAttributes = require "BooleanStateConfiguration.server.attributes" -local BooleanStateConfigurationTypes = require "BooleanStateConfiguration.types" +local BooleanStateConfigurationServerAttributes = require "embedded_clusters.BooleanStateConfiguration.server.attributes" +local BooleanStateConfigurationTypes = require "embedded_clusters.BooleanStateConfiguration.types" local BooleanStateConfiguration = {} diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua similarity index 95% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua index ea28550422..b2c9b67fbb 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/CurrentSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua index dc343735dc..760d7f397c 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/DefaultSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua similarity index 87% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua index 83db4bc404..3786904abf 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SensorFault.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SensorFault.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SensorFault = { ID = 0x0007, NAME = "SensorFault", - base_type = require "BooleanStateConfiguration.types.SensorFaultBitmap", + base_type = require "embedded_clusters.BooleanStateConfiguration.types.SensorFaultBitmap", } function SensorFault:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua index c80177d043..12bff3e4a9 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/SupportedSensitivityLevels.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua index 47ef55963b..daca1b7707 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("BooleanStateConfiguration.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.BooleanStateConfiguration.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua similarity index 97% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua index 3a4cb77058..dbc08cc0ed 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/Feature.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua index c9399e50f1..9661734152 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/SensorFaultBitmap.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/SensorFaultBitmap.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua index 79aae9c6e9..bc6835b4ac 100644 --- a/drivers/SmartThings/matter-sensor/src/BooleanStateConfiguration/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/BooleanStateConfiguration/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("BooleanStateConfiguration.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.BooleanStateConfiguration.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua index f5109f8943..4de97147e4 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonDioxideConcentrationMeasurementServerAttributes = require "CarbonDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua index 93583c2080..0206213e6f 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua index e8cdb487f5..a6e1f24d1d 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonMonoxideConcentrationMeasurementServerAttributes = require "CarbonMonoxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonMonoxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonMonoxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua index 2307e8977d..1a7e7b508c 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua similarity index 92% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua index 596d1bfe80..cb4cffa2d2 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ConcentrationMeasurementServerAttributes = require "ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurementTypes = require "ConcentrationMeasurement.types" +local ConcentrationMeasurementServerAttributes = require "embedded_clusters.ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurementTypes = require "embedded_clusters.ConcentrationMeasurement.types" local ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua index e7023a336c..e4f88c8491 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua index c658d2d3aa..2ab739841f 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua index 3d50e8b97b..0fa14745c0 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua index a1a0092151..19cde9aa55 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua similarity index 98% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua index 9aa2413903..0bb19bec62 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua index b1264d72b8..02b4f727df 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/LevelValueEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua index c8302c5cc4..6efd90901a 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua index f6da1e6b62..c339f17414 100644 --- a/drivers/SmartThings/matter-sensor/src/ConcentrationMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/ConcentrationMeasurement/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ConcentrationMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ConcentrationMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua index 5920a9dc66..cdfd1d597e 100644 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local FormaldehydeConcentrationMeasurementServerAttributes = require "FormaldehydeConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local FormaldehydeConcentrationMeasurementServerAttributes = require "embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local FormaldehydeConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua index 37900b0fb1..3dee13abe3 100644 --- a/drivers/SmartThings/matter-sensor/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("FormaldehydeConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua index b60e71050a..eae9be65f0 100644 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local NitrogenDioxideConcentrationMeasurementServerAttributes = require "NitrogenDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local NitrogenDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local NitrogenDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-sensor/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua index 06c3d6dd55..c82517d362 100644 --- a/drivers/SmartThings/matter-sensor/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua index 83fd04857e..c49ea94b39 100644 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local OzoneConcentrationMeasurementServerAttributes = require "OzoneConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local OzoneConcentrationMeasurementServerAttributes = require "embedded_clusters.OzoneConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local OzoneConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua index fe0048cd99..918b680495 100644 --- a/drivers/SmartThings/matter-sensor/src/OzoneConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("OzoneConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.OzoneConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua index 98eebd407e..3b333b5417 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm10ConcentrationMeasurementServerAttributes = require "Pm10ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm10ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm10ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm10ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua index 55f08c7e43..3b1e6617a4 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm10ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm10ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm10ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua index 0b3caa3bd2..b2e6656a9a 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm1ConcentrationMeasurementServerAttributes = require "Pm1ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm1ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm1ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm1ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua index f668e41a07..2635da32a6 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm1ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm1ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm1ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua index 5234346d60..e6e6144f94 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm25ConcentrationMeasurementServerAttributes = require "Pm25ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm25ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm25ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm25ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua index 2c7d5fce7b..5c432da0ec 100644 --- a/drivers/SmartThings/matter-sensor/src/Pm25ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm25ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm25ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua index d47c239242..8b1661152a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua @@ -15,9 +15,9 @@ -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. local cluster_base = require "st.matter.cluster_base" -local PressureMeasurementServerAttributes = require "PressureMeasurement.server.attributes" -local PressureMeasurementServerCommands = require "PressureMeasurement.server.commands" -local PressureMeasurementTypes = require "PressureMeasurement.types" +local PressureMeasurementServerAttributes = require "embedded_clusters.PressureMeasurement.server.attributes" +local PressureMeasurementServerCommands = require "embedded_clusters.PressureMeasurement.server.commands" +local PressureMeasurementTypes = require "embedded_clusters.PressureMeasurement.types" --- @class PressureMeasurement --- @alias PressureMeasurement diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AcceptedCommandList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/AttributeList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/EventList.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxMeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MaxScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinMeasuredValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/MinScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Scale.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Scale.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledTolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledTolerance.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/ScaledValue.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Tolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/Tolerance.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua similarity index 95% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua index 34c1959039..51844f7b7f 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua @@ -18,7 +18,7 @@ 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("PressureMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua index 3bb136bcb5..94f1c0849a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/commands/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua @@ -18,7 +18,7 @@ local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("PressureMeasurement.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua index c6fec3b6b5..fa444e129a 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/server/events/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua @@ -18,7 +18,7 @@ local event_mt = {} event_mt.__event_cache = {} event_mt.__index = function(self, key) if event_mt.__event_cache[key] == nil then - local req_loc = string.format("PressureMeasurement.server.events.%s", key) + local req_loc = string.format("embedded_clusters.PressureMeasurement.server.events.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua diff --git a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua index 682c6db041..f83681fdb8 100644 --- a/drivers/SmartThings/matter-sensor/src/PressureMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua @@ -18,7 +18,7 @@ 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("PressureMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.PressureMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua index 2a4cc04a0d..3d4b21d602 100644 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local RadonConcentrationMeasurementServerAttributes = require "RadonConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local RadonConcentrationMeasurementServerAttributes = require "embedded_clusters.RadonConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local RadonConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua index b83ef67bfc..8aa225e510 100644 --- a/drivers/SmartThings/matter-sensor/src/RadonConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("RadonConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.RadonConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua similarity index 90% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua index 6246ac2ceb..b492c28f18 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local SmokeCoAlarmServerAttributes = require "SmokeCoAlarm.server.attributes" -local SmokeCoAlarmServerCommands = require "SmokeCoAlarm.server.commands" -local SmokeCoAlarmTypes = require "SmokeCoAlarm.types" +local SmokeCoAlarmServerAttributes = require "embedded_clusters.SmokeCoAlarm.server.attributes" +local SmokeCoAlarmServerCommands = require "embedded_clusters.SmokeCoAlarm.server.commands" +local SmokeCoAlarmTypes = require "embedded_clusters.SmokeCoAlarm.types" local SmokeCoAlarm = {} diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua index 9129921b7b..4ba713dcc4 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/BatteryAlert.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/BatteryAlert.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local BatteryAlert = { ID = 0x0003, NAME = "BatteryAlert", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function BatteryAlert:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua index 0f99496b4f..db21d19922 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/COState.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/COState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local COState = { ID = 0x0002, NAME = "COState", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function COState:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua similarity index 94% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua index 2a4a8ed949..2c7cfdde88 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/HardwareFaultAlert.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua index b193c3673e..e97da91b05 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeSensitivityLevel.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SmokeSensitivityLevel = { ID = 0x000B, NAME = "SmokeSensitivityLevel", - base_type = require "SmokeCoAlarm.types.SensitivityEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.SensitivityEnum", } function SmokeSensitivityLevel:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua similarity index 88% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua index 08b04a9534..4879810c61 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/SmokeState.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/SmokeState.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local SmokeState = { ID = 0x0001, NAME = "SmokeState", - base_type = require "SmokeCoAlarm.types.AlarmStateEnum", + base_type = require "embedded_clusters.SmokeCoAlarm.types.AlarmStateEnum", } function SmokeState:new_value(...) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua similarity index 93% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua index d9ab8d2c25..42af52c87b 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/TestInProgress.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/TestInProgress.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua index 6cc54ec5e5..f08cf54fd0 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("SmokeCoAlarm.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.SmokeCoAlarm.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua similarity index 96% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua index 49454a078c..e8d892d1c3 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/SelfTestRequest.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/SelfTestRequest.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua index 6d768c069c..f1ff1ab72a 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/server/commands/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("SmokeCoAlarm.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.SmokeCoAlarm.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua similarity index 92% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua index 7352dbcd6e..ac20cac525 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/AlarmStateEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/AlarmStateEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua similarity index 95% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua index 22c7465a71..1918a3be00 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/Feature.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua index 4ba098ccfa..5728278657 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/SensitivityEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/SensitivityEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua similarity index 60% rename from drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua index 8467396dc8..027aaaf344 100644 --- a/drivers/SmartThings/matter-sensor/src/SmokeCoAlarm/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/SmokeCoAlarm/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("SmokeCoAlarm.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.SmokeCoAlarm.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua similarity index 89% rename from drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua index a99c1dea50..cad49a14e1 100644 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua index c0c1b65c37..b67cbcd7b6 100644 --- a/drivers/SmartThings/matter-sensor/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 5b9d88149e..4f6b204aea 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -1,222 +1,68 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" -local log = require "log" local clusters = require "st.matter.clusters" -local im = require "st.matter.interaction_model" local MatterDriver = require "st.matter.driver" -local utils = require "st.utils" -local embedded_cluster_utils = require "embedded-cluster-utils" +local version = require "version" + +local fields = require "sensor_utils.fields" +local device_cfg = require "sensor_utils.device_configuration" +local attribute_handlers = require "sensor_handlers.attribute_handlers" -- This can be removed once LuaLibs supports the PressureMeasurement cluster if not pcall(function(cluster) return clusters[cluster] end, "PressureMeasurement") then - clusters.PressureMeasurement = require "PressureMeasurement" + clusters.PressureMeasurement = require "embedded_clusters.PressureMeasurement" end -- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end -- Include driver-side definitions when lua libs api version is < 11 if version.api < 11 then - clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" -end - -local TEMP_BOUND_RECEIVED = "__temp_bound_received" -local TEMP_MIN = "__temp_min" -local TEMP_MAX = "__temp_max" -local FLOW_BOUND_RECEIVED = "__flow_bound_received" -local FLOW_MIN = "__flow_min" -local FLOW_MAX = "__flow_max" - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end -local BOOLEAN_DEVICE_TYPE_INFO = { - ["RAIN_SENSOR"] = { id = 0x0044, sensitivity_preference = "rainSensitivity", sensitivity_max = "rainMax" }, - ["WATER_FREEZE_DETECTOR"] = { id = 0x0041, sensitivity_preference = "freezeSensitivity", sensitivity_max = "freezeMax" }, - ["WATER_LEAK_DETECTOR"] = { id = 0x0043, sensitivity_preference = "leakSensitivity", sensitivity_max = "leakMax" }, - ["CONTACT_SENSOR"] = { id = 0x0015, sensitivity_preference = "N/A", sensitivity_max = "N/A" }, -} - -local ORDERED_DEVICE_TYPE_INFO = { - "RAIN_SENSOR", - "WATER_FREEZE_DETECTOR", - "WATER_LEAK_DETECTOR", - "CONTACT_SENSOR" -} - -local function set_boolean_device_type_per_endpoint(driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - if dt.device_type_id == info.id then - device:set_field(dt_name, ep.endpoint_id, { persist = true }) - device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) - end - end - end - end -end - -local function supports_sensitivity_preferences(device) - local preference_names = "" - local sensitivity_eps = embedded_cluster_utils.get_endpoints(device, clusters.BooleanStateConfiguration.ID, - {feature_bitmap = clusters.BooleanStateConfiguration.types.Feature.SENSITIVITY_LEVEL}) - if sensitivity_eps and #sensitivity_eps > 0 then - for _, dt_name in ipairs(ORDERED_DEVICE_TYPE_INFO) do - for _, sensitivity_ep in pairs(sensitivity_eps) do - if device:get_field(dt_name) == sensitivity_ep and BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference ~= "N/A" then - preference_names = preference_names .. "-" .. BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference - end - end - end - end - return preference_names -end - -local function match_profile(driver, device, battery_supported) - local profile_name = "" - - if device:supports_capability(capabilities.contactSensor) then - profile_name = profile_name .. "-contact" - end - - if device:supports_capability(capabilities.illuminanceMeasurement) then - profile_name = profile_name .. "-illuminance" - end - - if device:supports_capability(capabilities.temperatureMeasurement) then - profile_name = profile_name .. "-temperature" - end - - if device:supports_capability(capabilities.relativeHumidityMeasurement) then - profile_name = profile_name .. "-humidity" - end - - if device:supports_capability(capabilities.atmosphericPressureMeasurement) then - profile_name = profile_name .. "-pressure" - end - - if device:supports_capability(capabilities.rainSensor) then - profile_name = profile_name .. "-rain" - end - - if device:supports_capability(capabilities.temperatureAlarm) then - profile_name = profile_name .. "-freeze" - end +local SensorLifecycleHandlers = {} - if device:supports_capability(capabilities.waterSensor) then - profile_name = profile_name .. "-leak" - end - - if device:supports_capability(capabilities.flowMeasurement) then - profile_name = profile_name .. "-flow" - end - - if device:supports_capability(capabilities.button) then - profile_name = profile_name .. "-button" - end - - if battery_supported == battery_support.BATTERY_PERCENTAGE then - profile_name = profile_name .. "-battery" - elseif battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - end - - if device:supports_capability(capabilities.hardwareFault) then - profile_name = profile_name .. "-fault" - end - - local concatenated_preferences = supports_sensitivity_preferences(device) - profile_name = profile_name .. concatenated_preferences - - if device:supports_capability(capabilities.motionSensor) then - local occupancy_support = "-motion" - -- If the Occupancy Sensing Cluster’s revision is >= 5 (corresponds to Lua Libs version 13+), and any of the AIR / RAD / RFS / VIS - -- features are supported by the device, use the presenceSensor capability. Otherwise, use the motionSensor capability. Currently, - -- presenceSensor only used for devices fingerprinting to the motion-illuminance-temperature-humidity-battery profile. - if profile_name == "-illuminance-temperature-humidity-battery" and version.api >= 13 then - if #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.ACTIVE_INFRARED}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RADAR}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RF_SENSING}) > 0 or - #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) then - occupancy_support = "-presence" - end - end - profile_name = occupancy_support .. profile_name - end - - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) -end - -local function do_configure(driver, device) +function SensorLifecycleHandlers.do_configure(driver, device) 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, {}) - attribute_list_read:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(attribute_list_read) + device:send(clusters.PowerSource.attributes.AttributeList:read()) else - match_profile(driver, device, battery_support.NO_BATTERY) + device_cfg.match_profile(driver, device, fields.battery_support.NO_BATTERY) end end -local function device_init(driver, device) - log.info("device init") - set_boolean_device_type_per_endpoint(driver, device) +function SensorLifecycleHandlers.device_init(driver, device) + device.log.info("device init") + device_cfg.set_boolean_device_type_per_endpoint(driver, device) device:subscribe() end -local function info_changed(driver, device, event, args) +function SensorLifecycleHandlers.info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - set_boolean_device_type_per_endpoint(driver, device) + device_cfg.set_boolean_device_type_per_endpoint(driver, device) device:subscribe() end if not device.preferences then return end - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do local dt_ep = device:get_field(dt_name) if dt_ep and info.sensitivity_preference and (device.preferences[info.sensitivity_preference] ~= args.old_st_store.preferences[info.sensitivity_preference]) then local sensitivity_preference = device.preferences[info.sensitivity_preference] @@ -234,244 +80,76 @@ local function info_changed(driver, device, event, args) end end -local function illuminance_attr_handler(driver, device, ib, response) - local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) -end - -local function temperature_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local temp = measured_value / 100.0 - local unit = "C" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, TEMP_BOUND_RECEIVED..TEMP_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local humidity = utils.round(measured_value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) - end -end - -local BOOLEAN_CAP_EVENT_MAP = { - [true] = { - ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.freeze(), - ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.wet(), - ["RAIN_SENSOR"] = capabilities.rainSensor.rain.detected(), - ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.closed(), - }, - [false] = { - ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.cleared(), - ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.dry(), - ["RAIN_SENSOR"] = capabilities.rainSensor.rain.undetected(), - ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.open(), - } -} - -local function boolean_attr_handler(driver, device, ib, response) - local name - for dt_name, _ in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - local dt_ep_id = device:get_field(dt_name) - if ib.endpoint_id == dt_ep_id then - name = dt_name - break - end - end - if name then - device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) - elseif device:supports_capability(capabilities.contactSensor) then - -- The generic case where no device type has been specified but the profile uses this capability. - device:emit_event_for_endpoint(ib.endpoint_id, BOOLEAN_CAP_EVENT_MAP[ib.data.value]["CONTACT_SENSOR"]) - else - log.error("No Boolean device type found on an endpoint, BooleanState handler aborted") - end -end - -local function supported_sensitivities_handler(driver, device, ib, response) - if not ib.data.value then - return - end - - for dt_name, info in pairs(BOOLEAN_DEVICE_TYPE_INFO) do - if device:get_field(dt_name) == ib.endpoint_id then - device:set_field(info.sensitivity_max, ib.data.value, {persist = true}) - end - end -end - -local function sensor_fault_handler(driver, device, ib, response) - if ib.data.value > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.detected()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.clear()) - end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - -local function battery_charge_level_attr_handler(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 power_source_attribute_list_handler(driver, device, ib, response) - 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 - match_profile(driver, device, battery_support.BATTERY_PERCENTAGE) - return - elseif attr.value == 0x0E then - match_profile(driver, device, battery_support.BATTERY_LEVEL) - return - end - end -end - -local function occupancy_attr_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()) - else - device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) - end -end - -local function pressure_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local kPa = utils.round(measured_value / 10.0) - local unit = "kPa" - device:emit_event(capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = kPa, unit = unit})) - end -end - -local function flow_attr_handler(driver, device, ib, response) - local measured_value = ib.data.value - if measured_value ~= nil then - local flow = measured_value / 10.0 - local unit = "m^3/h" - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) - end -end - -local flow_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local flow_bound = ib.data.value / 10.0 - local unit = "m^3/h" - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) - local min = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id) - local max = get_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MIN, ib.endpoint_id, nil) - set_field_for_endpoint(device, FLOW_BOUND_RECEIVED..FLOW_MAX, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) - end - end - end -end - local matter_driver_template = { lifecycle_handlers = { - init = device_init, - infoChanged = info_changed, - doConfigure = do_configure, + doConfigure = SensorLifecycleHandlers.do_configure, + init = SensorLifecycleHandlers.device_init, + infoChanged = SensorLifecycleHandlers.info_changed, }, matter_handlers = { attr = { - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.BooleanState.ID] = { + [clusters.BooleanState.attributes.StateValue.ID] = attribute_handlers.boolean_state_value_handler }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temperature_attr_handler, - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MIN), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(TEMP_MAX), + [clusters.BooleanStateConfiguration.ID] = { + [clusters.BooleanStateConfiguration.attributes.SensorFault.ID] = attribute_handlers.sensor_fault_handler, + [clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels.ID] = attribute_handlers.supported_sensitivity_levels_handler, + }, + [clusters.FlowMeasurement.ID] = { + [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.flow_measured_value_handler, + [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.flow_measured_value_bounds_factory(fields.FLOW_MIN), + [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.flow_measured_value_bounds_factory(fields.FLOW_MAX) }, [clusters.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.illuminance_measured_value_handler }, - [clusters.BooleanState.ID] = { - [clusters.BooleanState.attributes.StateValue.ID] = boolean_attr_handler + [clusters.OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = attribute_handlers.occupancy_measured_value_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, - }, - [clusters.OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, [clusters.PressureMeasurement.ID] = { - [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = pressure_attr_handler, + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.pressure_measured_value_handler, }, - [clusters.BooleanStateConfiguration.ID] = { - [clusters.BooleanStateConfiguration.attributes.SensorFault.ID] = sensor_fault_handler, - [clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels.ID] = supported_sensitivities_handler, + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.humidity_measured_value_handler + }, + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_measured_value_handler, + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MIN), + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.TEMP_MAX), }, [clusters.Thermostat.ID] = { - [clusters.Thermostat.attributes.LocalTemperature.ID] = temperature_attr_handler + [clusters.Thermostat.attributes.LocalTemperature.ID] = attribute_handlers.temperature_measured_value_handler -- TemperatureMeasurement.MeasuredValue handler can support this attibute }, - [clusters.FlowMeasurement.ID] = { - [clusters.FlowMeasurement.attributes.MeasuredValue.ID] = flow_attr_handler, - [clusters.FlowMeasurement.attributes.MinMeasuredValue.ID] = flow_attr_handler_factory(FLOW_MIN), - [clusters.FlowMeasurement.attributes.MaxMeasuredValue.ID] = flow_attr_handler_factory(FLOW_MAX) - } } }, - -- TODO Once capabilities all have default handlers move this info there, and - -- use `supported_capabilities` subscribed_attributes = { - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, - clusters.Thermostat.attributes.LocalTemperature + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel, + clusters.SmokeCoAlarm.attributes.BatteryAlert, + }, + [capabilities.contactSensor.ID] = { + clusters.BooleanState.attributes.StateValue + }, + [capabilities.flowMeasurement.ID] = { + clusters.FlowMeasurement.attributes.MeasuredValue, + clusters.FlowMeasurement.attributes.MinMeasuredValue, + clusters.FlowMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.hardwareFault.ID] = { + clusters.BooleanStateConfiguration.attributes.SensorFault, + -- THESE ARE USED IN THE CASE OF THE SMOKE CO ALARM. + -- TODO: move this unique subscription logic into the subdriver + clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, + clusters.SmokeCoAlarm.attributes.BatteryAlert, + clusters.PowerSource.attributes.BatChargeLevel, }, [capabilities.illuminanceMeasurement.ID] = { clusters.IlluminanceMeasurement.attributes.MeasuredValue @@ -482,119 +160,114 @@ local matter_driver_template = { [capabilities.presenceSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy }, - [capabilities.contactSensor.ID] = { - clusters.BooleanState.attributes.StateValue + [capabilities.rainSensor.ID] = { + clusters.BooleanState.attributes.StateValue, }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - clusters.SmokeCoAlarm.attributes.BatteryAlert, + [capabilities.temperatureAlarm.ID] = { + clusters.BooleanState.attributes.StateValue, }, - [capabilities.atmosphericPressureMeasurement.ID] = { - clusters.PressureMeasurement.attributes.MeasuredValue + [capabilities.temperatureMeasurement.ID] = { + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + clusters.Thermostat.attributes.LocalTemperature }, + [capabilities.waterSensor.ID] = { + clusters.BooleanState.attributes.StateValue, + }, + -- AIR QUALITY SENSOR SPECIFIC CAPABILITIES -- [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.carbonDioxideHealthConcern.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, }, [capabilities.carbonDioxideMeasurement.ID] = { clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.carbonMonoxideHealthConcern.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit + [capabilities.carbonMonoxideMeasurement.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, + [capabilities.dustHealthConcern.ID] = { + clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit + [capabilities.dustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, + [capabilities.fineDustHealthConcern.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.fineDustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, }, [capabilities.formaldehydeHealthConcern.ID] = { clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.formaldehydeMeasurement.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, + [capabilities.nitrogenDioxideHealthConcern.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, + [capabilities.nitrogenDioxideMeasurement.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit }, - [capabilities.fineDustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.ozoneHealthConcern.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, + [capabilities.ozoneMeasurement.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, + clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, + [capabilities.radonHealthConcern.ID] = { + clusters.RadonConcentrationMeasurement.attributes.LevelValue, }, [capabilities.radonMeasurement.ID] = { clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.tvocHealthConcern.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue }, [capabilities.tvocMeasurement.ID] = { clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, + [capabilities.veryFineDustHealthConcern.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, }, - [capabilities.smokeDetector.ID] = { - clusters.SmokeCoAlarm.attributes.SmokeState, - clusters.SmokeCoAlarm.attributes.TestInProgress, + [capabilities.veryFineDustSensor.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, }, + -- SMOKE CO ALARM SPECIFIC CAPABILITIES -- [capabilities.carbonMonoxideDetector.ID] = { clusters.SmokeCoAlarm.attributes.COState, clusters.SmokeCoAlarm.attributes.TestInProgress, }, - [capabilities.hardwareFault.ID] = { - clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, - clusters.BooleanStateConfiguration.attributes.SensorFault, - }, - [capabilities.waterSensor.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.temperatureAlarm.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.rainSensor.ID] = { - clusters.BooleanState.attributes.StateValue, - }, - [capabilities.flowMeasurement.ID] = { - clusters.FlowMeasurement.attributes.MeasuredValue, - clusters.FlowMeasurement.attributes.MinMeasuredValue, - clusters.FlowMeasurement.attributes.MaxMeasuredValue + [capabilities.smokeDetector.ID] = { + clusters.SmokeCoAlarm.attributes.SmokeState, + clusters.SmokeCoAlarm.attributes.TestInProgress, }, }, subscribed_events = { @@ -604,8 +277,7 @@ local matter_driver_template = { clusters.Switch.events.MultiPressComplete, } }, - capability_handlers = { - }, + capability_handlers = {}, supported_capabilities = { capabilities.temperatureMeasurement, capabilities.contactSensor, @@ -624,9 +296,9 @@ local matter_driver_template = { capabilities.flowMeasurement, }, sub_drivers = { - require("air-quality-sensor"), - require("smoke-co-alarm"), - require("bosch-button-contact") + require("sub_drivers.air_quality_sensor"), + require("sub_drivers.smoke_co_alarm"), + require("sub_drivers.bosch_button_contact") } } diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..9bba480855 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -0,0 +1,204 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" +local sensor_utils = require "sensor_utils.utils" +local fields = require "sensor_utils.fields" +local device_cfg = require "sensor_utils.device_configuration" +local version = require "version" + +local AttributeHandlers = {} + + +-- [[ ILLUMINANCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.illuminance_measured_value_handler(driver, device, ib, response) + local lux = math.floor(10 ^ ((ib.data.value - 1) / 10000)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.illuminanceMeasurement.illuminance(lux)) +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local temp = measured_value / 100.0 + local unit = "C" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperature({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..minOrMax, ib.endpoint_id, temp) + local min = sensor_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id) + local max = sensor_utils.get_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MIN, ib.endpoint_id, nil) + sensor_utils.set_field_for_endpoint(device, fields.TEMP_BOUND_RECEIVED..fields.TEMP_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.humidity_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local humidity = st_utils.round(measured_value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) + end +end + + +-- [[ BOOLEAN STATE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.boolean_state_value_handler(driver, device, ib, response) + local name + for dt_name, _ in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + local dt_ep_id = device:get_field(dt_name) + if ib.endpoint_id == dt_ep_id then + name = dt_name + break + end + end + if name then + device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value][name]) + elseif device:supports_capability(capabilities.contactSensor) then + -- The generic case where no device type has been specified but the profile uses this capability. + device:emit_event_for_endpoint(ib.endpoint_id, fields.BOOLEAN_CAP_EVENT_MAP[ib.data.value]["CONTACT_SENSOR"]) + else + device.log.error("No Boolean device type found on an endpoint, BooleanState handler aborted") + end +end + + +-- [[ BOOLEAN STATE CONFIGURATION CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.sensor_fault_handler(driver, device, ib, response) + if ib.data.value > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.detected()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.hardwareFault.hardwareFault.clear()) + end +end + +function AttributeHandlers.supported_sensitivity_levels_handler(driver, device, ib, response) + if ib.data.value then + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + if device:get_field(dt_name) == ib.endpoint_id then + device:set_field(info.sensitivity_max, ib.data.value, {persist = true}) + end + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(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 + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + 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 + device_cfg.match_profile(driver, device, fields.battery_support.BATTERY_PERCENTAGE) + return + elseif attr.value == 0x0E then + device_cfg.match_profile(driver, device, fields.battery_support.BATTERY_LEVEL) + return + end + end +end + + +-- [[ OCCUPANCY SENSING CLUSTER ATTRIBUTES ]] -- + +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()) + else + device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) + end +end + + +-- [[ PRESSURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.pressure_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local kPa = st_utils.round(measured_value / 10.0) + local unit = "kPa" + device:emit_event(capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = kPa, unit = unit})) + end +end + + +-- [[ FLOW MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.flow_measured_value_handler(driver, device, ib, response) + local measured_value = ib.data.value + if measured_value ~= nil then + local flow = measured_value / 10.0 + local unit = "m^3/h" + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flow({value = flow, unit = unit})) + end +end + +function AttributeHandlers.flow_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local flow_bound = ib.data.value / 10.0 + local unit = "m^3/h" + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..minOrMax, ib.endpoint_id, flow_bound) + local min = sensor_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id) + local max = sensor_utils.get_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.flowMeasurement.flowRange({ value = { minimum = min, maximum = max }, unit = unit })) + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MIN, ib.endpoint_id, nil) + sensor_utils.set_field_for_endpoint(device, fields.FLOW_BOUND_RECEIVED..fields.FLOW_MAX, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min flow measurement %d that is not lower than the reported max flow measurement %d", min, max)) + end + end + end +end + +return AttributeHandlers \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua new file mode 100644 index 0000000000..2c5f38524a --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/device_configuration.lua @@ -0,0 +1,124 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sensor_utils.fields" +local version = require "version" + +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end + +local DeviceConfiguration = {} + +function DeviceConfiguration.set_boolean_device_type_per_endpoint(driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + for dt_name, info in pairs(fields.BOOLEAN_DEVICE_TYPE_INFO) do + if dt.device_type_id == info.id then + device:set_field(dt_name, ep.endpoint_id, { persist = true }) + device:send(clusters.BooleanStateConfiguration.attributes.SupportedSensitivityLevels:read(device, ep.endpoint_id)) + end + end + end + end +end + +local function supports_sensitivity_preferences(device) + local preference_names = "" + local sensitivity_eps = embedded_cluster_utils.get_endpoints(device, clusters.BooleanStateConfiguration.ID, + {feature_bitmap = clusters.BooleanStateConfiguration.types.Feature.SENSITIVITY_LEVEL}) + if sensitivity_eps and #sensitivity_eps > 0 then + for _, dt_name in ipairs(fields.ORDERED_DEVICE_TYPE_INFO) do + for _, sensitivity_ep in pairs(sensitivity_eps) do + if device:get_field(dt_name) == sensitivity_ep and fields.BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference ~= "N/A" then + preference_names = preference_names .. "-" .. fields.BOOLEAN_DEVICE_TYPE_INFO[dt_name].sensitivity_preference + end + end + end + end + return preference_names +end + +function DeviceConfiguration.match_profile(driver, device, battery_supported) + local profile_name = "" + + if device:supports_capability(capabilities.contactSensor) then + profile_name = profile_name .. "-contact" + end + + if device:supports_capability(capabilities.illuminanceMeasurement) then + profile_name = profile_name .. "-illuminance" + end + + if device:supports_capability(capabilities.temperatureMeasurement) then + profile_name = profile_name .. "-temperature" + end + + if device:supports_capability(capabilities.relativeHumidityMeasurement) then + profile_name = profile_name .. "-humidity" + end + + if device:supports_capability(capabilities.atmosphericPressureMeasurement) then + profile_name = profile_name .. "-pressure" + end + + if device:supports_capability(capabilities.rainSensor) then + profile_name = profile_name .. "-rain" + end + + if device:supports_capability(capabilities.temperatureAlarm) then + profile_name = profile_name .. "-freeze" + end + + if device:supports_capability(capabilities.waterSensor) then + profile_name = profile_name .. "-leak" + end + + if device:supports_capability(capabilities.flowMeasurement) then + profile_name = profile_name .. "-flow" + end + + if device:supports_capability(capabilities.button) then + profile_name = profile_name .. "-button" + end + + if battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + elseif battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + end + + if device:supports_capability(capabilities.hardwareFault) then + profile_name = profile_name .. "-fault" + end + + local concatenated_preferences = supports_sensitivity_preferences(device) + profile_name = profile_name .. concatenated_preferences + + if device:supports_capability(capabilities.motionSensor) then + local occupancy_support = "-motion" + -- If the Occupancy Sensing Cluster’s revision is >= 5 (corresponds to Lua Libs version 13+), and any of the AIR / RAD / RFS / VIS + -- features are supported by the device, use the presenceSensor capability. Otherwise, use the motionSensor capability. Currently, + -- presenceSensor only used for devices fingerprinting to the motion-illuminance-temperature-humidity-battery profile. + if profile_name == "-illuminance-temperature-humidity-battery" and version.api >= 13 then + if #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.ACTIVE_INFRARED}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RADAR}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.RF_SENSING}) > 0 or + #device:get_endpoints(clusters.OccupancySensing.ID, {feature_bitmap = clusters.OccupancySensing.types.Feature.VISION}) then + occupancy_support = "-presence" + end + end + profile_name = occupancy_support .. profile_name + end + + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) +end + +return DeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua similarity index 70% rename from drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua rename to drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua index 244ff77fc2..e2384098d9 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/embedded_cluster_utils.lua @@ -1,25 +1,28 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.matter.clusters" local utils = require "st.utils" -- Include driver-side definitions when lua libs api version is < 10 local version = require "version" if version.api < 10 then - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" - clusters.SmokeCoAlarm = require "SmokeCoAlarm" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" end if version.api < 11 then - clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" end local embedded_cluster_utils = {} diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua new file mode 100644 index 0000000000..f0b2a5c7a6 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/fields.lua @@ -0,0 +1,50 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local SensorFields = {} + +SensorFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" +SensorFields.TEMP_MIN = "__temp_min" +SensorFields.TEMP_MAX = "__temp_max" +SensorFields.FLOW_BOUND_RECEIVED = "__flow_bound_received" +SensorFields.FLOW_MIN = "__flow_min" +SensorFields.FLOW_MAX = "__flow_max" + +SensorFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +SensorFields.BOOLEAN_DEVICE_TYPE_INFO = { + ["RAIN_SENSOR"] = { id = 0x0044, sensitivity_preference = "rainSensitivity", sensitivity_max = "rainMax" }, + ["WATER_FREEZE_DETECTOR"] = { id = 0x0041, sensitivity_preference = "freezeSensitivity", sensitivity_max = "freezeMax" }, + ["WATER_LEAK_DETECTOR"] = { id = 0x0043, sensitivity_preference = "leakSensitivity", sensitivity_max = "leakMax" }, + ["CONTACT_SENSOR"] = { id = 0x0015, sensitivity_preference = "N/A", sensitivity_max = "N/A" }, +} + +SensorFields.ORDERED_DEVICE_TYPE_INFO = { + "RAIN_SENSOR", + "WATER_FREEZE_DETECTOR", + "WATER_LEAK_DETECTOR", + "CONTACT_SENSOR" +} + +SensorFields.BOOLEAN_CAP_EVENT_MAP = { + [true] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.freeze(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.wet(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.detected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.closed(), + }, + [false] = { + ["WATER_FREEZE_DETECTOR"] = capabilities.temperatureAlarm.temperatureAlarm.cleared(), + ["WATER_LEAK_DETECTOR"] = capabilities.waterSensor.water.dry(), + ["RAIN_SENSOR"] = capabilities.rainSensor.rain.undetected(), + ["CONTACT_SENSOR"] = capabilities.contactSensor.contact.open(), + } +} + +return SensorFields diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua new file mode 100644 index 0000000000..5a0421fb0c --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua @@ -0,0 +1,24 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local utils = {} + +function utils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function utils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function utils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +return utils diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua new file mode 100644 index 0000000000..f1ea0506d0 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua @@ -0,0 +1,88 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.fields" +local version = require "version" + +local DeviceConfiguration = {} + +function DeviceConfiguration.supported_level_measurements(device) + local measurement_caps, level_caps = {}, {} + for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + local cap_id = cap.ID + local cluster = fields.CONCENTRATION_MEASUREMENT_MAP[cap][2] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + table.insert(level_caps, cap_id) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + table.insert(measurement_caps, cap_id) + end + end + end + return measurement_caps, level_caps +end + +-- Match Modular Profile +function DeviceConfiguration.match_profile(device) + 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 + + if #temp_eps > 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + local measurement_caps, level_caps = DeviceConfiguration.supported_level_measurements(device) + + 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}) + + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + 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) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +return DeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua new file mode 100644 index 0000000000..4cb435d8dd --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua @@ -0,0 +1,178 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + + +local AirQualitySensorFields = {} + +AirQualitySensorFields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C + +AirQualitySensorFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" + +AirQualitySensorFields.units_required = { + clusters.CarbonMonoxideConcentrationMeasurement, + clusters.CarbonDioxideConcentrationMeasurement, + clusters.NitrogenDioxideConcentrationMeasurement, + clusters.OzoneConcentrationMeasurement, + clusters.FormaldehydeConcentrationMeasurement, + clusters.Pm1ConcentrationMeasurement, + clusters.Pm25ConcentrationMeasurement, + clusters.Pm10ConcentrationMeasurement, + clusters.RadonConcentrationMeasurement, + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement +} + +AirQualitySensorFields.supported_profiles = +{ + "aqs", + "aqs-temp-humidity-all-level-all-meas", + "aqs-temp-humidity-all-level", + "aqs-temp-humidity-all-meas", + "aqs-temp-humidity-co2-pm25-tvoc-meas", + "aqs-temp-humidity-co2-pm1-pm25-pm10-meas", + "aqs-temp-humidity-tvoc-level-pm25-meas", + "aqs-temp-humidity-tvoc-meas", +} + +AirQualitySensorFields.CONCENTRATION_MEASUREMENT_MAP = { + [capabilities.carbonMonoxideMeasurement] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, "N/A"}, + [capabilities.carbonMonoxideHealthConcern] = {"-co", clusters.CarbonMonoxideConcentrationMeasurement, capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues}, + [capabilities.carbonDioxideMeasurement] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, "N/A"}, + [capabilities.carbonDioxideHealthConcern] = {"-co2", clusters.CarbonDioxideConcentrationMeasurement, capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues}, + [capabilities.nitrogenDioxideMeasurement] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, "N/A"}, + [capabilities.nitrogenDioxideHealthConcern] = {"-no2", clusters.NitrogenDioxideConcentrationMeasurement, capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues}, + [capabilities.ozoneMeasurement] = {"-ozone", clusters.OzoneConcentrationMeasurement, "N/A"}, + [capabilities.ozoneHealthConcern] = {"-ozone", clusters.OzoneConcentrationMeasurement, capabilities.ozoneHealthConcern.supportedOzoneValues}, + [capabilities.formaldehydeMeasurement] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, "N/A"}, + [capabilities.formaldehydeHealthConcern] = {"-ch2o", clusters.FormaldehydeConcentrationMeasurement, capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues}, + [capabilities.veryFineDustSensor] = {"-pm1", clusters.Pm1ConcentrationMeasurement, "N/A"}, + [capabilities.veryFineDustHealthConcern] = {"-pm1", clusters.Pm1ConcentrationMeasurement, capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues}, + [capabilities.fineDustSensor] = {"-pm25", clusters.Pm25ConcentrationMeasurement, "N/A"}, + [capabilities.fineDustHealthConcern] = {"-pm25", clusters.Pm25ConcentrationMeasurement, capabilities.fineDustHealthConcern.supportedFineDustValues}, + [capabilities.dustSensor] = {"-pm10", clusters.Pm10ConcentrationMeasurement, "N/A"}, + [capabilities.dustHealthConcern] = {"-pm10", clusters.Pm10ConcentrationMeasurement, capabilities.dustHealthConcern.supportedDustValues}, + [capabilities.radonMeasurement] = {"-radon", clusters.RadonConcentrationMeasurement, "N/A"}, + [capabilities.radonHealthConcern] = {"-radon", clusters.RadonConcentrationMeasurement, capabilities.radonHealthConcern.supportedRadonValues}, + [capabilities.tvocMeasurement] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, "N/A"}, + [capabilities.tvocHealthConcern] = {"-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement, capabilities.tvocHealthConcern.supportedTvocValues}, +} + + +AirQualitySensorFields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING = { + capabilities.carbonMonoxideMeasurement, + capabilities.carbonMonoxideHealthConcern, + capabilities.carbonDioxideMeasurement, + capabilities.carbonDioxideHealthConcern, + capabilities.nitrogenDioxideMeasurement, + capabilities.nitrogenDioxideHealthConcern, + capabilities.ozoneMeasurement, + capabilities.ozoneHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.formaldehydeHealthConcern, + capabilities.veryFineDustSensor, + capabilities.veryFineDustHealthConcern, + capabilities.fineDustSensor, + capabilities.fineDustHealthConcern, + capabilities.dustSensor, + capabilities.dustHealthConcern, + capabilities.radonMeasurement, + capabilities.radonHealthConcern, + capabilities.tvocMeasurement, + capabilities.tvocHealthConcern, +} + +AirQualitySensorFields.units = { + PPM = 0, + PPB = 1, + PPT = 2, + MGM3 = 3, + UGM3 = 4, + NGM3 = 5, + PM3 = 6, + BQM3 = 7, + PCIL = 0xFF -- not in matter spec +} + +local units = AirQualitySensorFields.units -- copy to remove the prefix in uses below + +AirQualitySensorFields.unit_strings = { + [units.PPM] = "ppm", + [units.PPB] = "ppb", + [units.PPT] = "ppt", + [units.MGM3] = "mg/m^3", + [units.NGM3] = "ng/m^3", + [units.UGM3] = "μg/m^3", + [units.BQM3] = "Bq/m^3", + [units.PCIL] = "pCi/L" +} + +AirQualitySensorFields.unit_default = { + [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, + [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, + [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, + [capabilities.ozoneMeasurement.NAME] = units.PPM, + [capabilities.formaldehydeMeasurement.NAME] = units.PPM, + [capabilities.veryFineDustSensor.NAME] = units.UGM3, + [capabilities.fineDustSensor.NAME] = units.UGM3, + [capabilities.dustSensor.NAME] = units.UGM3, + [capabilities.radonMeasurement.NAME] = units.BQM3, + [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb +} + +-- All ConcentrationMeasurement clusters inherit from the same base cluster definitions, +-- so CarbonMonoxideConcentrationMeasurement is used below but the same enum types exist +-- in all ConcentrationMeasurement clusters +AirQualitySensorFields.level_strings = { + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", +} + +AirQualitySensorFields.conversion_tables = { + [units.PPM] = { + [units.PPM] = function(value) return utils.round(value) end, + [units.PPB] = function(value) return utils.round(value * (10^3)) end + }, + [units.PPB] = { + [units.PPM] = function(value) return utils.round(value/(10^3)) end, + [units.PPB] = function(value) return utils.round(value) end + }, + [units.PPT] = { + [units.PPM] = function(value) return utils.round(value/(10^6)) end + }, + [units.MGM3] = { + [units.UGM3] = function(value) return utils.round(value * (10^3)) end + }, + [units.UGM3] = { + [units.UGM3] = function(value) return utils.round(value) end + }, + [units.NGM3] = { + [units.UGM3] = function(value) return utils.round(value/(10^3)) end + }, + [units.BQM3] = { + [units.PCIL] = function(value) return utils.round(value/37) end + } +} + +return AirQualitySensorFields 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 new file mode 100644 index 0000000000..fea5e99e48 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua @@ -0,0 +1,264 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" +local log = require "log" + +local fields = require "sub_drivers.air_quality_sensor.fields" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end + + +-- SUBDRIVER UTILS -- + +local air_quality_sensor_utils = {} + +function air_quality_sensor_utils.is_matter_air_quality_sensor(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then + return true + end + end + end + + return false + end + +function air_quality_sensor_utils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + + +-- AIR QUALITY SENSOR LIFECYCLE HANDLERS -- + +local AirQualitySensorLifecycleHandlers = {} + +function AirQualitySensorLifecycleHandlers.do_configure(driver, device) + -- we have to read the unit before reports of values will do anything + for _, cluster in ipairs(fields.units_required) do + device:send(cluster.attributes.MeasurementUnit:read(device)) + end + if version.api >= 14 and version.rpc >= 8 then + local modular_device_cfg = require "sub_drivers.air_quality_sensor.device_configuration" + modular_device_cfg.match_profile(device) + else + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +function AirQualitySensorLifecycleHandlers.driver_switched(driver, device) + -- we have to read the unit before reports of values will do anything + for _, cluster in ipairs(fields.units_required) do + device:send(cluster.attributes.MeasurementUnit:read(device)) + end + if version.api >= 14 and version.rpc >= 8 then + local modular_device_cfg = require "sub_drivers.air_quality_sensor.device_configuration" + modular_device_cfg.match_profile(device) + else + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +function AirQualitySensorLifecycleHandlers.device_init(driver, device) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", air_quality_sensor_utils.supports_capability_by_id_modular) + end + device:subscribe() +end + +function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id 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", air_quality_sensor_utils.supports_capability_by_id_modular) + end + device:subscribe() + end +end + +-- ATTRIBUTE HANDLERS -- + +local sub_driver_handlers = {} + +function sub_driver_handlers.measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +local function unit_conversion(value, from_unit, to_unit) + local conversion_function = fields.conversion_tables[from_unit][to_unit] + if conversion_function == nil 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 1 + end + + if value == nil then + log.info_with( {hub_logs = true} , "unit conversion value is nil") + return 1 + end + return conversion_function(value) +end + +function sub_driver_handlers.measured_value_factory(capability_name, attribute, target_unit) + return function(driver, device, ib, response) + local reporting_unit = device:get_field(capability_name.."_unit") + + if reporting_unit == nil then + reporting_unit = fields.unit_default[capability_name] + device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) + end + + if reporting_unit then + local value = unit_conversion(ib.data.value, reporting_unit, target_unit) + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = 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]})) + end + end + end +end + +function sub_driver_handlers.level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value])) + end +end + +function sub_driver_handlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + +function sub_driver_handlers.pressure_measured_value_handler(driver, device, ib, response) + local pressure = utils.round(ib.data.value / 10.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) +end + + +-- SUBDRIVER TEMPLATE -- + +local matter_air_quality_sensor_handler = { + NAME = "matter-air-quality-sensor", + lifecycle_handlers = { + doConfigure = AirQualitySensorLifecycleHandlers.do_configure, + driverSwitched = AirQualitySensorLifecycleHandlers.driver_switched, + infoChanged = AirQualitySensorLifecycleHandlers.info_changed, + init = AirQualitySensorLifecycleHandlers.device_init, + }, + matter_handlers = { + attr = { + [clusters.AirQuality.ID] = { + [clusters.AirQuality.attributes.AirQuality.ID] = sub_driver_handlers.air_quality_handler, + }, + [clusters.CarbonDioxideConcentrationMeasurement.ID] = { + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + }, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + }, + [clusters.FormaldehydeConcentrationMeasurement.ID] = { + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + }, + [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + }, + [clusters.OzoneConcentrationMeasurement.ID] = { + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.ozoneMeasurement.NAME), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern) + }, + [clusters.Pm1ConcentrationMeasurement.ID] = { + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.veryFineDustSensor.NAME), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + }, + [clusters.Pm10ConcentrationMeasurement.ID] = { + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.dustSensor.NAME), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + }, + [clusters.Pm25ConcentrationMeasurement.ID] = { + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.fineDustSensor.NAME), + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + }, + [clusters.PressureMeasurement.ID] = { + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.pressure_measured_value_handler + }, + [clusters.RadonConcentrationMeasurement.ID] = { + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.radonMeasurement.NAME), + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.radonHealthConcern.radonHealthConcern) + }, + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.tvocMeasurement.NAME), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern) + } + } + }, + can_handle = air_quality_sensor_utils.is_matter_air_quality_sensor +} + +return matter_air_quality_sensor_handler diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua new file mode 100644 index 0000000000..aa76cb8d0e --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua @@ -0,0 +1,121 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.fields" +local sensor_utils = require "sensor_utils.utils" + +local LegacyDeviceConfiguration = {} + +local function set_supported_health_concern_values(device, setter_function, cluster, cluster_ep) + -- read_datatype_value works since all the healthConcern capabilities' datatypes are equivalent to the one in airQualityHealthConcern + local read_datatype_value = capabilities.airQualityHealthConcern.airQualityHealthConcern + local supported_values = {read_datatype_value.unknown.NAME, read_datatype_value.good.NAME, read_datatype_value.unhealthy.NAME} + if cluster == clusters.AirQuality then + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.FAIR }) > 0 then + table.insert(supported_values, 3, read_datatype_value.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MODERATE }) > 0 then + table.insert(supported_values, 4, read_datatype_value.slightlyUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.VERY_POOR }) > 0 then + table.insert(supported_values, read_datatype_value.veryUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.EXTREMELY_POOR }) > 0 then + table.insert(supported_values, read_datatype_value.hazardous.NAME) + end + else -- ConcentrationMeasurement clusters + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then + table.insert(supported_values, 3, read_datatype_value.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then + table.insert(supported_values, read_datatype_value.hazardous.NAME) + end + end + device:emit_event_for_endpoint(cluster_ep, setter_function(supported_values, { visibility = { displayed = false }})) +end + +function LegacyDeviceConfiguration.create_level_measurement_profile(device) + local meas_name, level_name = "", "" + for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + local cap_id = cap.ID + local cluster = fields.CONCENTRATION_MEASUREMENT_MAP[cap][2] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + level_name = level_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1] + set_supported_health_concern_values(device, fields.CONCENTRATION_MEASUREMENT_MAP[cap][3], cluster, attr_eps[1]) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + meas_name = meas_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1] + end + end + end + return meas_name, level_name +end + +-- MATCH STATIC PROFILE +function LegacyDeviceConfiguration.match_profile(device) + 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 profile_name = "aqs" + local aq_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + set_supported_health_concern_values(device, capabilities.airQualityHealthConcern.supportedAirQualityValues, clusters.AirQuality, aq_eps[1]) + + if #temp_eps > 0 then + profile_name = profile_name .. "-temp" + end + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + local meas_name, level_name = LegacyDeviceConfiguration.create_level_measurement_profile(device) + + -- If all endpoints are supported, use '-all' in the profile name so that it + -- remains under the profile name character limit + if level_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then + level_name = "-all" + end + if level_name ~= "" then + profile_name = profile_name .. level_name .. "-level" + end + + -- If all endpoints are supported, use '-all' in the profile name so that it + -- remains under the profile name character limit + if meas_name == "-co-co2-no2-ozone-ch2o-pm1-pm25-pm10-radon-tvoc" then + meas_name = "-all" + end + if meas_name ~= "" then + profile_name = profile_name .. meas_name .. "-meas" + end + + if not sensor_utils.tbl_contains(fields.supported_profiles, profile_name) then + device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s", profile_name)) + + local function meas_find(sub_name) + return string.match(meas_name, sub_name) ~= nil + end + + -- try to best match to existing profiles + -- these checks, meas_find("co%-") and meas_find("co$"), match the string to co and NOT co2. + if meas_find("co%-") or meas_find("co$") or meas_find("no2") or meas_find("ozone") or meas_find("ch2o") or + meas_find("pm1") or meas_find("pm10") or meas_find("radon") then + profile_name = "aqs-temp-humidity-all-meas" + elseif #humidity_eps > 0 or #temp_eps > 0 or meas_find("co2") or meas_find("pm25") or meas_find("tvoc") then + profile_name = "aqs-temp-humidity-co2-pm25-tvoc-meas" + else + -- device only supports air quality at this point + profile_name = "aqs" + end + 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 + +return LegacyDeviceConfiguration \ No newline at end of file diff --git a/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua similarity index 91% rename from drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua rename to drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua index 1dfd3960ce..9b4635d87c 100644 --- a/drivers/SmartThings/matter-sensor/src/bosch-button-contact/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -155,8 +144,8 @@ local Bosch_Button_Contact_Sensor = { [clusters.Switch.events.MultiPressComplete.ID] = multi_press_complete_event_handler } }, - }, - can_handle = is_bosch_button_contact, + }, + can_handle = is_bosch_button_contact, } return Bosch_Button_Contact_Sensor diff --git a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua similarity index 63% rename from drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua rename to drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua index f622a3a4ba..374bbd5ad9 100644 --- a/drivers/SmartThings/matter-sensor/src/smoke-co-alarm/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua @@ -1,20 +1,22 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" +local version = require "version" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local sensor_utils = require "sensor_utils.utils" +local fields = require "sensor_utils.fields" + +if version.api < 10 then + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" +end + + +-- SUBDRIVER UTILS -- + +local smoke_co_alarm_utils = {} local CARBON_MONOXIDE_MEASUREMENT_UNIT = "CarbonMonoxideConcentrationMeasurement_unit" local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 @@ -23,18 +25,7 @@ local HardwareFaultAlert = "__HardwareFaultAlert" local BatteryAlert = "__BatteryAlert" local BatteryLevel = "__BatteryLevel" -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local version = require "version" -if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" -end - -local function is_matter_smoke_co_alarm(opts, driver, device) +function smoke_co_alarm_utils.is_matter_smoke_co_alarm(opts, driver, device) for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do if dt.device_type_id == SMOKE_CO_ALARM_DEVICE_TYPE_ID then @@ -46,15 +37,6 @@ local function is_matter_smoke_co_alarm(opts, driver, device) return false end -local tbl_contains = function(t, val) - for _, v in pairs(t) do - if v == val then - return true - end - end - return false -end - local supported_profiles = { "co", @@ -71,7 +53,7 @@ local supported_profiles = "smoke-co-temp-humidity-comeas-battery" } -local function match_profile(device, battery_supported) +function smoke_co_alarm_utils.match_profile(device, battery_supported) local smoke_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.SMOKE_ALARM}) local co_eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID, {feature_bitmap = clusters.SmokeCoAlarm.types.Feature.CO_ALARM}) local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) @@ -100,14 +82,14 @@ local function match_profile(device, battery_supported) if #co_level_eps > 0 then profile_name = profile_name .. "-colevel" end - if battery_supported == battery_support.BATTERY_PERCENTAGE then + if battery_supported == fields.battery_support.BATTERY_PERCENTAGE then profile_name = profile_name .. "-battery" end -- remove leading "-" profile_name = string.sub(profile_name, 2) - if tbl_contains(supported_profiles, profile_name) then + if sensor_utils.tbl_contains(supported_profiles, profile_name) then device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) else device.log.warn_with({hub_logs=true}, string.format("No matching profile for device. Tried to use profile %s.", profile_name)) @@ -124,11 +106,25 @@ local function match_profile(device, battery_supported) device:try_update_metadata({profile = profile_name}) end -local function device_init(driver, device) + +-- SMOKE CO ALARM LIFECYCLE HANDLERS -- + +local SmokeLifeycleHandlers = {} + +function SmokeLifeycleHandlers.device_init(driver, device) device:subscribe() end -local function info_changed(self, device, event, args) +function SmokeLifeycleHandlers.do_configure(driver, device) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + device:send(clusters.PowerSource.attributes.AttributeList:read()) + else + smoke_co_alarm_utils.match_profile(device, fields.battery_support.NO_BATTERY) + end +end + +function SmokeLifeycleHandlers.info_changed(self, device, event, args) if device.preferences then if device.preferences["certifiedpreferences.smokeSensorSensitivity"] ~= args.old_st_store.preferences["certifiedpreferences.smokeSensorSensitivity"] then local eps = embedded_cluster_utils.get_endpoints(device, clusters.SmokeCoAlarm.ID) @@ -151,8 +147,12 @@ local function info_changed(self, device, event, args) end end --- Matter Handlers -- -local function binary_state_handler_factory(zeroEvent, nonZeroEvent) + +-- CLUSTER ATTRIBUTE HANDLERS -- + +local sub_driver_handlers = {} + +function sub_driver_handlers.smoke_co_alarm_state_factory(zeroEvent, nonZeroEvent) return function(driver, device, ib, response) if ib.data.value == 0 and zeroEvent ~= nil then device:emit_event_for_endpoint(ib.endpoint_id, zeroEvent) @@ -162,7 +162,7 @@ local function binary_state_handler_factory(zeroEvent, nonZeroEvent) end end -local function test_in_progress_event_handler(driver, device, ib, response) +function sub_driver_handlers.test_in_progress_handler(driver, device, ib, response) if device:supports_capability(capabilities.smokeDetector) then if ib.data.value then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.smokeDetector.smoke.tested()) @@ -179,7 +179,7 @@ local function test_in_progress_event_handler(driver, device, ib, response) end end -local function carbon_monoxide_attr_handler(driver, device, ib, response) +function sub_driver_handlers.carbon_monoxide_measured_value_handler(driver, device, ib, response) local value = ib.data.value local unit = device:get_field(CARBON_MONOXIDE_MEASUREMENT_UNIT) if unit == clusters.CarbonMonoxideConcentrationMeasurement.types.MeasurementUnitEnum.PPB then @@ -191,12 +191,12 @@ local function carbon_monoxide_attr_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel({value = value, unit = "ppm"})) end -local function carbon_monoxide_unit_attr_handler(driver, device, ib, response) +function sub_driver_handlers.carbon_monoxide_measurement_unit_handler(driver, device, ib, response) local unit = ib.data.value device:set_field(CARBON_MONOXIDE_MEASUREMENT_UNIT, unit, { persist = true }) end -local function hardware_fault_capability_handler(device) +function sub_driver_handlers.hardware_fault_capability_handler(device) local batLevel, batAlert = device:get_field(BatteryLevel), device:get_field(BatteryAlert) if device:get_field(HardwareFaultAlert) == true or (batLevel and batAlert and (batAlert > batLevel)) then device:emit_event(capabilities.hardwareFault.hardwareFault.detected()) @@ -205,31 +205,31 @@ local function hardware_fault_capability_handler(device) end end -local function hardware_fault_alert_handler(driver, device, ib, response) +function sub_driver_handlers.hardware_fault_alert_handler(driver, device, ib, response) device:set_field(HardwareFaultAlert, ib.data.value, {persist = true}) - hardware_fault_capability_handler(device) + sub_driver_handlers.hardware_fault_capability_handler(device) end -local function battery_alert_attr_handler(driver, device, ib, response) +function sub_driver_handlers.battery_alert_handler(driver, device, ib, response) device:set_field(BatteryAlert, ib.data.value, {persist = true}) - hardware_fault_capability_handler(device) + sub_driver_handlers.hardware_fault_capability_handler(device) end -local function power_source_attribute_list_handler(driver, device, ib, response) +function sub_driver_handlers.power_source_attribute_list_handler(driver, device, ib, response) 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 - match_profile(device, battery_support.BATTERY_PERCENTAGE) + smoke_co_alarm_utils.match_profile(device, fields.battery_support.BATTERY_PERCENTAGE) return elseif attr.value == 0x0E then - match_profile(device, battery_support.BATTERY_LEVEL) + smoke_co_alarm_utils.match_profile(device, fields.battery_support.BATTERY_LEVEL) return end end end -local function handle_battery_charge_level(driver, device, ib, response) +function sub_driver_handlers.bat_charge_level_handler(driver, device, ib, response) device:set_field(BatteryLevel, ib.data.value, {persist = true}) -- value used in hardware_fault_capability_handler if device:supports_capability(capabilities.batteryLevel) then -- check required since attribute is subscribed to even without batteryLevel support, to set the field above if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then @@ -242,80 +242,36 @@ local function handle_battery_charge_level(driver, device, ib, response) 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 do_configure(driver, device) - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps > 0 then - device:send(clusters.PowerSource.attributes.AttributeList:read()) - else - match_profile(device, battery_support.NO_BATTERY) - end -end +-- SUBDRIVER TEMPLATE -- local matter_smoke_co_alarm_handler = { NAME = "matter-smoke-co-alarm", lifecycle_handlers = { - init = device_init, - infoChanged = info_changed, - doConfigure = do_configure + init = SmokeLifeycleHandlers.device_init, + infoChanged = SmokeLifeycleHandlers.info_changed, + doConfigure = SmokeLifeycleHandlers.do_configure }, matter_handlers = { attr = { - [clusters.SmokeCoAlarm.ID] = { - [clusters.SmokeCoAlarm.attributes.SmokeState.ID] = binary_state_handler_factory(capabilities.smokeDetector.smoke.clear(), capabilities.smokeDetector.smoke.detected()), - [clusters.SmokeCoAlarm.attributes.COState.ID] = binary_state_handler_factory(capabilities.carbonMonoxideDetector.carbonMonoxide.clear(), capabilities.carbonMonoxideDetector.carbonMonoxide.detected()), - [clusters.SmokeCoAlarm.attributes.BatteryAlert.ID] = battery_alert_attr_handler, - [clusters.SmokeCoAlarm.attributes.TestInProgress.ID] = test_in_progress_event_handler, - [clusters.SmokeCoAlarm.attributes.HardwareFaultAlert.ID] = hardware_fault_alert_handler, - }, [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = carbon_monoxide_attr_handler, - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = carbon_monoxide_unit_attr_handler, + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.carbon_monoxide_measured_value_handler, + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.carbon_monoxide_measurement_unit_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, + [clusters.PowerSource.attributes.AttributeList.ID] = sub_driver_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = sub_driver_handlers.bat_charge_level_handler, + }, + [clusters.SmokeCoAlarm.ID] = { + [clusters.SmokeCoAlarm.attributes.BatteryAlert.ID] = sub_driver_handlers.battery_alert_handler, + [clusters.SmokeCoAlarm.attributes.COState.ID] = sub_driver_handlers.smoke_co_alarm_state_factory(capabilities.carbonMonoxideDetector.carbonMonoxide.clear(), capabilities.carbonMonoxideDetector.carbonMonoxide.detected()), + [clusters.SmokeCoAlarm.attributes.HardwareFaultAlert.ID] = sub_driver_handlers.hardware_fault_alert_handler, + [clusters.SmokeCoAlarm.attributes.SmokeState.ID] = sub_driver_handlers.smoke_co_alarm_state_factory(capabilities.smokeDetector.smoke.clear(), capabilities.smokeDetector.smoke.detected()), + [clusters.SmokeCoAlarm.attributes.TestInProgress.ID] = sub_driver_handlers.test_in_progress_handler, }, }, }, - subscribed_attributes = { - [capabilities.smokeDetector.ID] = { - clusters.SmokeCoAlarm.attributes.SmokeState, - clusters.SmokeCoAlarm.attributes.TestInProgress, - }, - [capabilities.carbonMonoxideDetector.ID] = { - clusters.SmokeCoAlarm.attributes.COState, - clusters.SmokeCoAlarm.attributes.TestInProgress, - }, - [capabilities.hardwareFault.ID] = { - clusters.SmokeCoAlarm.attributes.HardwareFaultAlert, - clusters.SmokeCoAlarm.attributes.BatteryAlert, - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.TemperatureMeasurement.attributes.MeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel, - }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining, - } - }, - can_handle = is_matter_smoke_co_alarm + can_handle = smoke_co_alarm_utils.is_matter_smoke_co_alarm } return matter_smoke_co_alarm_handler \ No newline at end of file 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 871aec64e2..fbf5babd5c 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -19,17 +8,23 @@ local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" local clusters = require "st.matter.clusters" -clusters.AirQuality = require "AirQuality" -clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" -clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" -clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" -clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" -clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" -clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" -clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" -clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" -clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" -clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +local version = require "version" + +-- Include driver-side definitions when lua libs api version is < 10 +if version.api < 10 then + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" +end local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), 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 d799c31b64..0f186ee655 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 b63abcea14..1ca2add2ff 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- package.path = package.path .. ";./?lua" -- package.loaded["path"] = dofile("mock_path.lua") local test = require "integration_test" 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 f6312dfc8f..6b1301cb96 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 7aae047a29..44a984b2a7 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 @@ -1,22 +1,15 @@ --- Copyright 2024 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. +-- Copyright © 2025 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" -clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" +local version = require "version" + +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end local mock_device_freeze_leak = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("freeze-leak-fault-freezeSensitivity-leakSensitivity.yml"), 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 68134ed677..f7b3d7afe0 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 @@ -1,22 +1,15 @@ --- Copyright 2022 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. +-- Copyright © 2025 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 PressureMeasurementCluster = require "PressureMeasurement" + +if not pcall(function(cluster) return clusters[cluster] end, + "PressureMeasurement") then + clusters.PressureMeasurement = require "embedded_clusters.PressureMeasurement" +end --Note all endpoints are being mapped to the main component -- in the matter-sensor driver. If any devices require invoke/write @@ -35,7 +28,7 @@ local matter_endpoints = { { endpoint_id = 1, clusters = { - {cluster_id = PressureMeasurementCluster.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.PressureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, }, device_types = { @@ -51,7 +44,7 @@ local mock_device = test.mock_device.build_test_matter_device({ local function test_init() test.mock_device.add_test_device(mock_device) - local subscribe_request = PressureMeasurementCluster.attributes.MeasuredValue:subscribe(mock_device) + local subscribe_request = clusters.PressureMeasurement.attributes.MeasuredValue:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end @@ -65,7 +58,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1054) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1054) } }, { @@ -78,7 +71,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1055) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 1055) } }, { @@ -91,7 +84,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - PressureMeasurementCluster.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 0) + clusters.PressureMeasurement.server.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 0) } }, { @@ -124,7 +117,7 @@ test.register_message_test( local function refresh_commands(dev) local req = clusters.PowerSource.attributes.BatPercentRemaining:read(dev) - req:merge(PressureMeasurementCluster.attributes.MeasuredValue:read(dev)) + req:merge(clusters.PressureMeasurement.attributes.MeasuredValue:read(dev)) return req end 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 835de811ae..21f8a0fa51 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 @@ -1,24 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2025 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 version = require "version" -clusters.BooleanStateConfiguration = require "BooleanStateConfiguration" +if version.api < 11 then + clusters.BooleanStateConfiguration = require "embedded_clusters.BooleanStateConfiguration" +end local mock_device_rain = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("rain-fault.yml"), 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 7116a03b92..a66e575310 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 72484efffe..8d3962de66 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 ed9718b947..b1e5d39248 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 7eb31d5c29..88af036e19 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" 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 86060ab442..c7cfe26c2a 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -18,11 +7,10 @@ local t_utils = require "integration_test.utils" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" local clusters = require "st.matter.clusters" -clusters.SmokeCoAlarm = require "SmokeCoAlarm" local version = require "version" if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ 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 b618b3e5d6..816552935e 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 @@ -1,28 +1,16 @@ --- Copyright 2024 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. +-- Copyright © 2025 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 uint32 = require "st.matter.data_types.Uint32" - local clusters = require "st.matter.clusters" -clusters.SmokeCoAlarm = require "SmokeCoAlarm" + local version = require "version" if version.api < 10 then - clusters.SmokeCoAlarm = require "SmokeCoAlarm" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" + clusters.SmokeCoAlarm = require "embedded_clusters.SmokeCoAlarm" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ @@ -69,6 +57,8 @@ local cluster_subscribe_list = { clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, clusters.PowerSource.attributes.BatPercentRemaining, + clusters.PowerSource.attributes.BatChargeLevel, + clusters.SmokeCoAlarm.attributes.BatteryAlert, } local function test_init() From 380effce6d1766b6c9167b202fd9d10bd80db426 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 4 Nov 2025 14:41:44 -0800 Subject: [PATCH 249/449] Merge pull request #2529 from SmartThingsCommunity/new_device/WWSTCERT-8703 WWSTCERT-8703 NodOn Zigbee ON/OFF Lighting Relay Switch --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index cb3890125d..236758b30d 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2380,6 +2380,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-1-20 deviceProfileName: basic-switch + - id: "NodOn/SIN-4-2-20" + deviceLabel: Zigbee ON/OFF Lighting Relay Switch + manufacturer: NodOn + model: SIN-4-2-20 + deviceProfileName: switch-level zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From ce46751bacf23ea6b89ba78221de389b7f677e85 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 3 Nov 2025 15:15:16 -0600 Subject: [PATCH 250/449] chore/zwave-switch lazy loading of sub drivers refactor --- adjuster.py | 81 +++++++++++++++++++ .../src/aeon-smart-strip/can_handle.lua | 19 +++++ .../src/aeon-smart-strip/fingerprints.lua | 3 + .../src/aeon-smart-strip/init.lua | 21 +---- .../src/aeotec-heavy-duty/can_handle.lua | 14 ++++ .../src/aeotec-heavy-duty/fingerprints.lua | 3 + .../src/aeotec-heavy-duty/init.lua | 16 +--- .../src/aeotec-smart-switch/can_handle.lua | 13 +++ .../src/aeotec-smart-switch/fingerprints.lua | 5 ++ .../src/aeotec-smart-switch/init.lua | 18 +---- .../src/dawon-smart-plug/can_handle.lua | 18 +++++ .../src/dawon-smart-plug/fingerprints.lua | 4 + .../src/dawon-smart-plug/init.lua | 20 +---- .../dawon-wall-smart-switch/can_handle.lua | 18 +++++ .../dawon-wall-smart-switch/fingerprints.lua | 8 ++ .../src/dawon-wall-smart-switch/init.lua | 26 +----- .../src/eaton-5-scene-keypad/can_handle.lua | 14 ++++ .../src/eaton-5-scene-keypad/fingerprints.lua | 3 + .../src/eaton-5-scene-keypad/init.lua | 16 +--- .../src/eaton-accessory-dimmer/can_handle.lua | 13 +++ .../eaton-accessory-dimmer/fingerprints.lua | 3 + .../src/eaton-accessory-dimmer/init.lua | 16 +--- .../src/eaton-anyplace-switch/can_handle.lua | 13 +++ .../eaton-anyplace-switch/fingerprints.lua | 3 + .../src/eaton-anyplace-switch/init.lua | 16 +--- .../src/ecolink-switch/can_handle.lua | 13 +++ .../src/ecolink-switch/fingerprints.lua | 7 ++ .../zwave-switch/src/ecolink-switch/init.lua | 20 +---- .../src/fibaro-double-switch/can_handle.lua | 13 +++ .../src/fibaro-double-switch/fingerprints.lua | 5 ++ .../src/fibaro-double-switch/init.lua | 18 +---- .../src/fibaro-single-switch/can_handle.lua | 13 +++ .../src/fibaro-single-switch/fingerprints.lua | 5 ++ .../src/fibaro-single-switch/init.lua | 18 +---- .../src/fibaro-wall-plug-us/can_handle.lua | 13 +++ .../src/fibaro-wall-plug-us/fingerprints.lua | 4 + .../src/fibaro-wall-plug-us/init.lua | 17 +--- drivers/SmartThings/zwave-switch/src/init.lua | 15 +--- .../can_handle.lua | 13 +++ .../fingerprints.lua | 10 +++ .../inovelli-2-channel-smart-plug/init.lua | 23 +----- .../src/inovelli-LED/can_handle.lua | 18 +++++ .../zwave-switch/src/inovelli-LED/init.lua | 22 +---- .../inovelli-lzw31sn/can_handle.lua | 16 ++++ .../inovelli-LED/inovelli-lzw31sn/init.lua | 16 +--- .../src/inovelli-LED/sub_drivers.lua | 5 ++ .../zwave-switch/src/lazy_load_subdriver.lua | 15 ++++ .../src/multi-metering-switch/can_handle.lua | 13 +++ .../multi-metering-switch/fingerprints.lua | 15 ++++ .../src/multi-metering-switch/init.lua | 28 +------ .../src/multichannel-device/can_handle.lua | 11 +++ .../src/multichannel-device/init.lua | 11 +-- .../src/qubino-switches/can_handle.lua | 11 +++ .../src/qubino-switches/fingerprints.lua | 9 +++ .../zwave-switch/src/qubino-switches/init.lua | 27 +------ .../qubino-dimmer/can_handle.lua | 12 +++ .../qubino-dimmer/fingerprints.lua | 6 ++ .../qubino-switches/qubino-dimmer/init.lua | 22 +---- .../qubino-din-dimmer/can_handle.lua | 9 +++ .../qubino-dimmer/qubino-din-dimmer/init.lua | 10 +-- .../qubino-dimmer/sub_drivers.lua | 5 ++ .../qubino-relays/can_handle.lua | 12 +++ .../qubino-relays/fingerprints.lua | 5 ++ .../qubino-switches/qubino-relays/init.lua | 23 +----- .../qubino-flush-1-relay/can_handle.lua | 11 +++ .../qubino-flush-1-relay/init.lua | 8 +- .../qubino-flush-1d-relay/can_handle.lua | 11 +++ .../qubino-flush-1d-relay/init.lua | 8 +- .../qubino-flush-2-relay/can_handle.lua | 11 +++ .../qubino-flush-2-relay/init.lua | 8 +- .../qubino-relays/sub_drivers.lua | 6 ++ .../src/qubino-switches/sub_drivers.lua | 6 ++ .../src/zooz-power-strip/can_handle.lua | 13 +++ .../src/zooz-power-strip/fingerprints.lua | 3 + .../src/zooz-power-strip/init.lua | 16 +--- .../zooz-zen-30-dimmer-relay/can_handle.lua | 13 +++ .../zooz-zen-30-dimmer-relay/fingerprints.lua | 3 + .../src/zooz-zen-30-dimmer-relay/init.lua | 16 +--- .../src/zwave-dual-switch/can_handle.lua | 13 +++ .../src/zwave-dual-switch/fingerprints.lua | 11 +++ .../src/zwave-dual-switch/init.lua | 24 +----- 81 files changed, 629 insertions(+), 464 deletions(-) create mode 100644 adjuster.py create mode 100644 drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua diff --git a/adjuster.py b/adjuster.py new file mode 100644 index 0000000000..de7f37f587 --- /dev/null +++ b/adjuster.py @@ -0,0 +1,81 @@ +import os +import sys +import re + +ignored_folders = ["test"] + +driver = os.getcwd() + "/drivers/SmartThings/zigbee-thermostat/" +subdriver_folders = list(filter(lambda x: "." not in x and x not in ignored_folders, os.listdir(driver + "src/"))) + +for subdriver in subdriver_folders: + path = driver + "src/" + subdriver + init_file = path + + new_init_buffer = [] + new_init = path + "/init.lua.new" + + capture_key = None + captures = {} + + end_counter = 0 + filename = path + "/init.lua" + print(filename) + with open(filename, 'r') as fd: + for line in fd: + + # unnamed can_handle function + if match := re.match(r"(\s*)can_handle\s*=\s*(function.*)",line): + capture_key = "can_handle.lua" + replacement = f"{match.group(1)}can_handle = require(\"{subdriver}.can_handle\")\n" + + print(match.groups()) + print(replacement) + new_init_buffer.append(replacement) + + # named function somewhere else + elif match := re.match(r"(\s*)can_handle\s*=\s*(.*)",line): + replacement = f"{match.group(1)}can_handle = require(\"{subdriver}.can_handle\")\n" + print(match.groups()) + print(replacement) + new_init_buffer.append(replacement) + print("Reread") + + + + + + elif capture_key: + # Create or append the line to the captured segment + try: + captures[capture_key].append(line) + except KeyError: + captures[capture_key] = [line] + print(f"{capture_key}{end_counter}:{line}",end="") + + if match := re.match(r"(?!\s*(?:else|elseif))(?=.*\b(?:then|if|for|function)\b).+$",line): + end_counter += 1 + elif match := re.match(r"\s*end\s*",line): + end_counter = max(end_counter-1,0) + + if end_counter == 0: + capture_key = None + # elif match := re.match(r"\s*local\s+(?:function|(?:(.+\S)\s*=))\s*(?:function)?\s*\(.*\)",line): + # end_counter += 1 + # capture_key = match.group(1) + # captures[capture_key] = [line] + # print(f"{capture_key}:{line}",end="") + # else: + # new_init_buffer.append(line) + + with open(filename, 'r') as fd: + for line in fd: + + for key in captures.keys(): + print(f"Key:{key}") + # for l in captures[key]: + # print(l,end="") + break + # print("="*20) + + # with open(new_init,'w') as fd: + # fd.writelines(new_init_buffer) \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua new file mode 100644 index 0000000000..4641be4e95 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua @@ -0,0 +1,19 @@ +local fingerprints = require("aeon-smart-strip.fingerprints") + +--- Determine whether the passed device is Aeon smart strip +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_aeon_smart_strip(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("aeon-smart-strip") + return true, subdriver + end + end + return false +end + + +return can_handle_aeon_smart_strip \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua new file mode 100644 index 0000000000..317928e619 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua @@ -0,0 +1,3 @@ +return { + {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS +} diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua index f3bafc0815..d957af2042 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua @@ -24,29 +24,10 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local AEON_SMART_STRIP_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS -} - local ENERGY_UNIT_KWH = "kWh" local ENERGY_UNIT_KVAH = "kVAh" local POWER_UNIT_WATT = "W" ---- Determine whether the passed device is Aeon smart strip ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_aeon_smart_strip(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_SMART_STRIP_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("aeon-smart-strip") - return true, subdriver - end - end - return false -end - local function binary_event_helper(self, device, cmd) local value = cmd.args.value and cmd.args.value or cmd.args.target_value local event = value == SwitchBinary.value.OFF_DISABLE and capabilities.switch.switch.off() or capabilities.switch.switch.on() @@ -108,7 +89,7 @@ local aeon_smart_strip = { [Meter.REPORT] = meter_report_handler } }, - can_handle = can_handle_aeon_smart_strip, + can_handle = require("aeon-smart-strip.can_handle"), } return aeon_smart_strip diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua new file mode 100644 index 0000000000..34ee438a5a --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua @@ -0,0 +1,14 @@ +local fingerprints = require("aeotec-heavy-duty.fingerprints") + + +local function can_handle(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, nil, fingerprint.model) then + local subdriver = require("aeotec-heavy-duty") + return true, subdriver + end + end + return false +end + +return can_handle \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua new file mode 100644 index 0000000000..726b336997 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua @@ -0,0 +1,3 @@ +return { + { mfr = 0x0086, model = 0x004E } +} diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua index 9a10b32c64..66d1778397 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua @@ -29,20 +29,6 @@ local LAST_REPORT_TIME = "LAST_REPORT_TIME" local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local FINGERPRINTS = { - { mfr = 0x0086, model = 0x004E } -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.mfr, nil, fingerprint.model) then - local subdriver = require("aeotec-heavy-duty") - return true, subdriver - end - end - return false -end - local function emit_power_consumption_report_event(device, value, channel) -- powerConsumptionReport report interval local current_time = os.time() @@ -128,7 +114,7 @@ local driver_template = { lifecycle_handlers = { infoChanged = info_changed }, - can_handle = can_handle + can_handle = require("aeotec-heavy-duty.can_handle") } return driver_template; \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua new file mode 100644 index 0000000000..907b92e64e --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("aeotec-smart-switch.fingerprints") + +local function can_handle(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, nil, fingerprint.prodId) then + local subdriver = require("aeotec-smart-switch") + return true, subdriver + end + end + return false +end + +return can_handle \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua new file mode 100644 index 0000000000..a30a29a55c --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua @@ -0,0 +1,5 @@ +return { + {mfr = 0x0086, prodId = 0x0060}, + {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU + {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua index d014188b46..e7daab6b86 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua @@ -28,22 +28,6 @@ local LAST_REPORT_TIME = "LAST_REPORT_TIME" local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local FINGERPRINTS = { - {mfr = 0x0086, prodId = 0x0060}, - {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU - {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.mfr, nil, fingerprint.prodId) then - local subdriver = require("aeotec-smart-switch") - return true, subdriver - end - end - return false -end - local function emit_power_consumption_report_event(device, value, channel) -- powerConsumptionReport report interval local current_time = os.time() @@ -144,7 +128,7 @@ local aeotec_smart_switch = { [Meter.REPORT] = meter_report_handler } }, - can_handle = can_handle + can_handle = require("aeotec-smart-switch.can_handle") } return aeotec_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua new file mode 100644 index 0000000000..ecf5acff5c --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua @@ -0,0 +1,18 @@ +local fingerprints = require("dawon-smart-plug.fingerprints") + +--- Determine whether the passed device is Dawon smart plug +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_dawon_smart_plug(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("dawon-smart-plug") + return true, subdriver + end + end + return false +end + +return can_handle_dawon_smart_plug \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua new file mode 100644 index 0000000000..6bf63ecff2 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua @@ -0,0 +1,4 @@ +return { + {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug + {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab +} diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua index 6280842f6b..da6bbc69af 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua @@ -18,25 +18,7 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local DAWON_SMART_PLUG_FINGERPRINTS = { - {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug - {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab -} ---- Determine whether the passed device is Dawon smart plug ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_dawon_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(DAWON_SMART_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("dawon-smart-plug") - return true, subdriver - end - end - return false -end --- Default handler for notification reports --- @@ -60,7 +42,7 @@ local dawon_smart_plug = { [Notification.REPORT] = notification_report_handler } }, - can_handle = can_handle_dawon_smart_plug + can_handle = require("dawon-smart-plug.can_handle") } return dawon_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua new file mode 100644 index 0000000000..b8bacfdc4f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua @@ -0,0 +1,18 @@ +local fingerprints = require("dawon-wall-smart-switch.fingerprints") + +--- Determine whether the passed device is Dawon wall smart switch +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("dawon-wall-smart-switch") + return true, subdriver + end + end + return false +end + +return can_handle_dawon_wall_smart_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua new file mode 100644 index 0000000000..075f7f9edd --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua @@ -0,0 +1,8 @@ +return { + {mfr = 0x018C, prod = 0x0061, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 KR + {mfr = 0x018C, prod = 0x0062, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 KR + {mfr = 0x018C, prod = 0x0063, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 KR + {mfr = 0x018C, prod = 0x0064, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 US + {mfr = 0x018C, prod = 0x0065, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 US + {mfr = 0x018C, prod = 0x0066, model = 0x0001} -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 US +} diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua index f7978ad54d..8e5c475321 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua @@ -24,30 +24,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({ version = --- @type st.zwave.CommandClass.SensorMultilevel local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) -local DAWON_WALL_SMART_SWITCH_FINGERPRINTS = { - {mfr = 0x018C, prod = 0x0061, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 KR - {mfr = 0x018C, prod = 0x0062, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 KR - {mfr = 0x018C, prod = 0x0063, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 KR - {mfr = 0x018C, prod = 0x0064, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 US - {mfr = 0x018C, prod = 0x0065, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 US - {mfr = 0x018C, prod = 0x0066, model = 0x0001} -- Dawon Multipurpose Sensor + Smart Switch endpoint 3 US -} - ---- Determine whether the passed device is Dawon wall smart switch ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(DAWON_WALL_SMART_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("dawon-wall-smart-switch") - return true, subdriver - end - end - return false -end - --- Default handler for notification reports --- --- @param self st.zwave.Driver @@ -97,7 +73,7 @@ local dawon_wall_smart_switch = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = can_handle_dawon_wall_smart_switch, + can_handle = require("dawon-wall-smart-switch.can_handle"), } return dawon_wall_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua new file mode 100644 index 0000000000..8ff3aab14f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua @@ -0,0 +1,14 @@ + +local fingerprints = require("eaton-5-scene-keypad.fingerprints") + +local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("eaton-5-scene-keypad") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_5_scene_keypad \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua new file mode 100644 index 0000000000..59e9d0d1a0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua @@ -0,0 +1,3 @@ +return { + {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad +} diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua index 29fccc9261..6c92b0895d 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua @@ -29,10 +29,6 @@ local SceneControllerConf = (require "st.zwave.CommandClass.SceneControllerConf" local INDICATOR_SWITCH_STATES = "Indicator_switch_states" -local EATON_5_SCENE_KEYPAD_FINGERPRINT = { - {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad -} - local function upsert_after_bit_update_at_index(device, bit_position, new_bit) local old_value = device:get_field(INDICATOR_SWITCH_STATES) or 0 local mask = ~(0x1 << (bit_position - 1)) @@ -108,16 +104,6 @@ local function do_configure(self, device) device:set_field(INDICATOR_SWITCH_STATES, 0, { persist = true}) end -local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_5_SCENE_KEYPAD_FINGERPRINT) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("eaton-5-scene-keypad") - return true, subdriver - end - end - return false -end - local eaton_5_scene_keypad = { NAME = "Eaton 5-Scene Keypad", zwave_handlers = { @@ -146,7 +132,7 @@ local eaton_5_scene_keypad = { lifecycle_handlers = { doConfigure = do_configure, }, - can_handle = can_handle_eaton_5_scene_keypad, + can_handle = require("eaton-5-scene-keypad.can_handle"), } return eaton_5_scene_keypad diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..fd4bfb8b4a --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("eaton-accessory-dimmer.fingerprints") + +local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("eaton-accessory-dimmer") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_accessory_dimmer \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua new file mode 100644 index 0000000000..44628d1d0a --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua @@ -0,0 +1,3 @@ +return { + {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua index 3a9d5c9a47..290cce85e4 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua @@ -24,20 +24,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) -local EATON_ACCESSORY_DIMMER_FINGERPRINTS = { - {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch -} - -local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("eaton-accessory-dimmer") - return true, subdriver - end - end - return false -end - local function dimmer_event(driver, device, cmd) local level = cmd.args.value and cmd.args.value or cmd.args.target_value @@ -110,7 +96,7 @@ local eaton_accessory_dimmer = { [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_set } }, - can_handle = can_handle_eaton_accessory_dimmer, + can_handle = require("eaton-accessory-dimmer.can_handle"), } return eaton_accessory_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua new file mode 100644 index 0000000000..33a0471740 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("eaton-anyplace-switch.fingerprints") + +local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("eaton-anyplace-switch") + return true, subdriver + end + end + return false +end + +return can_handle_eaton_anyplace_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua new file mode 100644 index 0000000000..0a20a7c98e --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua @@ -0,0 +1,3 @@ +return { + { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch +} diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua index b2110623f6..c965d82ed7 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua @@ -20,20 +20,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local switch_utils = require "switch_utils" -local EATON_ANYPLACE_SWITCH_FINGERPRINTS = { - { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch -} - -local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(EATON_ANYPLACE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("eaton-anyplace-switch") - return true, subdriver - end - end - return false -end - local function basic_set_handler(self, device, cmd) if cmd.args.value == 0xFF then device:emit_event(capabilities.switch.switch.on()) @@ -76,7 +62,7 @@ local eaton_anyplace_switch = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_eaton_anyplace_switch + can_handle = require("eaton-anyplace-switch.can_handle") } return eaton_anyplace_switch diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua new file mode 100644 index 0000000000..023e6756b3 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("ecolink-switch.fingerprints") + +local function can_handle_ecolink(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("ecolink-switch") + return true, subdriver + end + end + return false +end + +return can_handle_ecolink \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua new file mode 100644 index 0000000000..1a9122d140 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua @@ -0,0 +1,7 @@ +return { + {mfr = 0x014A, prod = 0x0006, model = 0x0002}, + {mfr = 0x014A, prod = 0x0006, model = 0x0003}, + {mfr = 0x014A, prod = 0x0006, model = 0x0004}, + {mfr = 0x014A, prod = 0x0006, model = 0x0005}, + {mfr = 0x014A, prod = 0x0006, model = 0x0006} +} diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua index 3e69399801..daff734562 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua @@ -16,24 +16,6 @@ local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) -local ECOLINK_FINGERPRINTS = { - {mfr = 0x014A, prod = 0x0006, model = 0x0002}, - {mfr = 0x014A, prod = 0x0006, model = 0x0003}, - {mfr = 0x014A, prod = 0x0006, model = 0x0004}, - {mfr = 0x014A, prod = 0x0006, model = 0x0005}, - {mfr = 0x014A, prod = 0x0006, model = 0x0006} -} - -local function can_handle_ecolink(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("ecolink-switch") - return true, subdriver - end - end - return false -end - local function basic_set_handler(driver, device, cmd) if cmd.args.value == 0xFF then device:emit_event(capabilities.switch.switch.on()) @@ -49,7 +31,7 @@ local ecolink_switch = { [Basic.SET] = basic_set_handler } }, - can_handle = can_handle_ecolink + can_handle = require("ecolink-switch.can_handle") } return ecolink_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua new file mode 100644 index 0000000000..d4c3fae1eb --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("fibaro-double-switch.fingerprints") + +local function can_handle_fibaro_double_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-double-switch") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_double_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua new file mode 100644 index 0000000000..50fa9404f3 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua @@ -0,0 +1,5 @@ +return { + {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua index 8bccc75464..1cec8ca95d 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua @@ -34,22 +34,6 @@ local ENDPOINTS = { child = 2 } -local FIBARO_DOUBLE_SWITCH_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch -} - -local function can_handle_fibaro_double_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_DOUBLE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-double-switch") - return true, subdriver - end - end - return false -end - local function do_refresh(driver, device, command) local component = command and command.component and command.component or "main" device:send_to_component(SwitchBinary:Get({}), component) @@ -140,7 +124,7 @@ local fibaro_double_switch = { init = device_init, added = device_added }, - can_handle = can_handle_fibaro_double_switch, + can_handle = require("fibaro-double-switch.can_handle") } return fibaro_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua new file mode 100644 index 0000000000..b629911db9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("fibaro-single-switch.fingerprints") + +local function can_handle_fibaro_single_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-single-switch") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_single_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua new file mode 100644 index 0000000000..de11fe944f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua @@ -0,0 +1,5 @@ +return { + {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua index e36703ccd6..dce2efda9e 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua @@ -28,22 +28,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = --- @type st.zwave.CommandClass.Basic local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) -local FIBARO_SINGLE_SWITCH_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch -} - -local function can_handle_fibaro_single_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_SINGLE_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-single-switch") - return true, subdriver - end - end - return false -end - local function central_scene_notification_handler(self, device, cmd) if cmd.src_channel == nil or cmd.src_channel == 0 then ButtonDefaults.zwave_handlers[cc.CENTRAL_SCENE][CentralScene.NOTIFICATION](self, device, cmd) @@ -94,7 +78,7 @@ local fibaro_single_switch = { [capabilities.switch.commands.off.NAME] = switch_handler_factory(0x00), } }, - can_handle = can_handle_fibaro_single_switch, + can_handle = require("fibaro-single-switch.can_handle") } return fibaro_single_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua new file mode 100644 index 0000000000..b3c7f03d8a --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("fibaro-wall-plug-us.fingerprints") + +local function can_handle_fibaro_wall_plug(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("fibaro-wall-plug-us") + return true, subdriver + end + end + return false +end + +return can_handle_fibaro_wall_plug \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua new file mode 100644 index 0000000000..463f8ce0fe --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua @@ -0,0 +1,4 @@ +return { + {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet + {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua index 6224421c75..0bb455b294 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua @@ -12,21 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -local FIBARO_WALL_PLUG_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet - {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet -} - -local function can_handle_fibaro_wall_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_WALL_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("fibaro-wall-plug-us") - return true, subdriver - end - end - return false -end - local function component_to_endpoint(device, component_id) if component_id == "main" then return {1} @@ -54,7 +39,7 @@ local fibaro_wall_plug = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_fibaro_wall_plug, + can_handle = require("fibaro-wall-plug-us.can_handle"), } return fibaro_wall_plug diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 3acae8ffe0..be00a73223 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -28,6 +28,8 @@ local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ ve local preferencesMap = require "preferences" local configurationsMap = require "configurations" +local lazy_load_if_possible = require "lazy_load_subdriver" + --- Map component to end_points(channels) --- --- @param device st.zwave.Device @@ -103,19 +105,6 @@ local function switch_multilevel_stop_level_change_handler(driver, device, cmd) device:send(SwitchMultilevel:Get({})) end -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - ------------------------------------------------------------------------------------------- -- Register message handlers and run driver ------------------------------------------------------------------------------------------- diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua new file mode 100644 index 0000000000..429fc4c28f --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("inovelli-2-channel-smart-plug.fingerprints") + +local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("inovelli-2-channel-smart-plug") + return true, subdriver + end + end + return false +end + +return can_handle_inovelli_2_channel_smart_plug \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua new file mode 100644 index 0000000000..145020c3df --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua @@ -0,0 +1,10 @@ +return { + {mfr = 0x015D, prod = 0x0221, model = 0x251C}, -- Show Home Outlet + {mfr = 0x0312, prod = 0x0221, model = 0x251C}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0xB221, model = 0x251C}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet + {mfr = 0x0312, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet + {mfr = 0x015D, prod = 0x2500, model = 0x2500}, -- Inovelli Outlet +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua index d97f85063c..8f86d4302a 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua @@ -24,27 +24,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = --- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) -local INOVELLI_2_CHANNEL_SMART_PLUG_FINGERPRINTS = { - {mfr = 0x015D, prod = 0x0221, model = 0x251C}, -- Show Home Outlet - {mfr = 0x0312, prod = 0x0221, model = 0x251C}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0xB221, model = 0x251C}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x0221, model = 0x611C}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet - {mfr = 0x0312, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet - {mfr = 0x015D, prod = 0x2500, model = 0x2500}, -- Inovelli Outlet -} - -local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) - for _, fingerprint in ipairs(INOVELLI_2_CHANNEL_SMART_PLUG_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("inovelli-2-channel-smart-plug") - return true, subdriver - end - end - return false -end - local function handle_main_switch_event(device, value) if value == SwitchBinary.value.ON_ENABLE then device:emit_event(capabilities.switch.switch.on()) @@ -125,7 +104,7 @@ local inovelli_2_channel_smart_plug = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_inovelli_2_channel_smart_plug, + can_handle = require("inovelli-2-channel-smart-plug.can_handle") } return inovelli_2_channel_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua new file mode 100644 index 0000000000..a9662f0c12 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua @@ -0,0 +1,18 @@ +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 +local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_inovelli_led(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + {INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_LZW31_PRODUCT_TYPE}, + INOVELLI_DIMMER_PRODUCT_ID + ) then + local subdriver = require("inovelli-LED") + return true, subdriver + end + return false +end + +return can_handle_inovelli_led \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua index 36bf1e0716..ffd57c716b 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua @@ -24,10 +24,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version= local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 local LED_GENERIC_SATURATION = 100 -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" local function huePercentToZwaveValue(value) @@ -78,18 +74,6 @@ local function set_color(driver, device, cmd) device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) end -local function can_handle_inovelli_led(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - {INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_LZW31_PRODUCT_TYPE}, - INOVELLI_DIMMER_PRODUCT_ID - ) then - local subdriver = require("inovelli-LED") - return true, subdriver - end - return false -end - local inovelli_led = { NAME = "Inovelli LED", zwave_handlers = { @@ -102,10 +86,8 @@ local inovelli_led = { [capabilities.colorControl.commands.setColor.NAME] = set_color } }, - can_handle = can_handle_inovelli_led, - sub_drivers = { - require("inovelli-LED/inovelli-lzw31sn") - } + can_handle = require("inovelli-LED.can_handle"), + sub_drivers = require("inovelli-LED.sub_drivers"), } return inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua new file mode 100644 index 0000000000..f7288340d7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua @@ -0,0 +1,16 @@ +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + INOVELLI_LZW31SN_PRODUCT_TYPE, + INOVELLI_DIMMER_PRODUCT_ID + ) then + return true, require("inovelli-LED.inovelli-lzw31sn") + end + return false +end + +return can_handle_inovelli_lzw31sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua index 307ee9b46a..355b4a4fd4 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua @@ -18,9 +18,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.CentralScene local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" local supported_button_values = { @@ -85,17 +82,6 @@ local function central_scene_notification_handler(self, device, cmd) end end -local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - INOVELLI_LZW31SN_PRODUCT_TYPE, - INOVELLI_DIMMER_PRODUCT_ID - ) then - return true - end - return false -end - local inovelli_led_lzw31sn = { NAME = "Inovelli LED LZW 31SN", zwave_handlers = { @@ -106,7 +92,7 @@ local inovelli_led_lzw31sn = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_inovelli_lzw31sn + can_handle = require("inovelli-LED.inovelli-lzw31sn.can_handle") } return inovelli_led_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua new file mode 100644 index 0000000000..a0d2b99754 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua @@ -0,0 +1,5 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("inovelli-LED.inovelli-lzw31sn"), +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..32ef997530 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua new file mode 100644 index 0000000000..036f555362 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("multi-metering-switch.fingerprints") + +local function can_handle_multi_metering_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("multi-metering-switch") + return true, subdriver + end + end + return false +end + +return can_handle_multi_metering_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua new file mode 100644 index 0000000000..9df75e4fb7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua @@ -0,0 +1,15 @@ +return { + {mfr = 0x0086, prod = 0x0003, model = 0x0084}, -- Aeotec Nano Switch 1 + {mfr = 0x0086, prod = 0x0103, model = 0x0084}, -- Aeotec Nano Switch 1 + {mfr = 0x0086, prod = 0x0203, model = 0x0084}, -- AU Aeotec Nano Switch 1 + {mfr = 0x027A, prod = 0xA000, model = 0xA004}, -- Zooz ZEN Power Strip 1 + {mfr = 0x015F, prod = 0x3102, model = 0x0201}, -- WYFY Touch 1-button Switch + {mfr = 0x015F, prod = 0x3102, model = 0x0202}, -- WYFY Touch 2-button Switch + {mfr = 0x015F, prod = 0x3102, model = 0x0204}, -- WYFY Touch 4-button Switch + {mfr = 0x015F, prod = 0x3111, model = 0x5102}, -- WYFY Touch 1-button Switch + {mfr = 0x015F, prod = 0x3121, model = 0x5102}, -- WYFY Touch 2-button Switch + {mfr = 0x015F, prod = 0x3141, model = 0x5102}, -- WYFY Touch 4-button Switch + {mfr = 0x0460, prod = 0x0002, model = 0x0081}, -- Shelly Wave 2PM + {mfr = 0x0460, prod = 0x0002, model = 0x008C}, -- Shelly Wave Pro 2 + {mfr = 0x0460, prod = 0x0002, model = 0x008D}, -- Shelly Wave Pro 2PM +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua index efe7d35893..1d76852411 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua @@ -31,32 +31,6 @@ local MULTI_METERING_SWITCH_CONFIGURATION_MAP = require "multi-metering-switch/m local PARENT_ENDPOINT = 1 -local MULTI_METERING_SWITCH_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x0084}, -- Aeotec Nano Switch 1 - {mfr = 0x0086, prod = 0x0103, model = 0x0084}, -- Aeotec Nano Switch 1 - {mfr = 0x0086, prod = 0x0203, model = 0x0084}, -- AU Aeotec Nano Switch 1 - {mfr = 0x027A, prod = 0xA000, model = 0xA004}, -- Zooz ZEN Power Strip 1 - {mfr = 0x015F, prod = 0x3102, model = 0x0201}, -- WYFY Touch 1-button Switch - {mfr = 0x015F, prod = 0x3102, model = 0x0202}, -- WYFY Touch 2-button Switch - {mfr = 0x015F, prod = 0x3102, model = 0x0204}, -- WYFY Touch 4-button Switch - {mfr = 0x015F, prod = 0x3111, model = 0x5102}, -- WYFY Touch 1-button Switch - {mfr = 0x015F, prod = 0x3121, model = 0x5102}, -- WYFY Touch 2-button Switch - {mfr = 0x015F, prod = 0x3141, model = 0x5102}, -- WYFY Touch 4-button Switch - {mfr = 0x0460, prod = 0x0002, model = 0x0081}, -- Shelly Wave 2PM - {mfr = 0x0460, prod = 0x0002, model = 0x008C}, -- Shelly Wave Pro 2 - {mfr = 0x0460, prod = 0x0002, model = 0x008D}, -- Shelly Wave Pro 2PM -} - -local function can_handle_multi_metering_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTI_METERING_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("multi-metering-switch") - return true, subdriver - end - end - return false -end - local function find_child(parent, ep_id) if ep_id == PARENT_ENDPOINT then return parent @@ -199,7 +173,7 @@ local multi_metering_switch = { init = device_init, added = device_added }, - can_handle = can_handle_multi_metering_switch, + can_handle = require("multi-metering-switch.can_handle"), } return multi_metering_switch diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua new file mode 100644 index 0000000000..db4ef36bb9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua @@ -0,0 +1,11 @@ +local capabilities = require "st.capabilities" + +local function can_handle_multichannel_device(opts, driver, device, ...) + if device:supports_capability(capabilities.zwMultichannel) then + local subdriver = require("multichannel-device") + return true, subdriver + end + return false +end + +return can_handle_multichannel_device \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua index 54349673e8..48c8b2837d 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua @@ -12,7 +12,6 @@ -- See the License for the specific language governing permissions and -- limitations under the License. local cc = require "st.zwave.CommandClass" -local capabilities = require "st.capabilities" local st_device = require "st.device" local MultiChannel = (require "st.zwave.CommandClass.MultiChannel")({ version = 3 }) local utils = require "st.utils" @@ -27,14 +26,6 @@ local map_device_class_to_profile = { [0xA1] = "generic-sensor" } -local function can_handle_multichannel_device(opts, driver, device, ...) - if device:supports_capability(capabilities.zwMultichannel) then - local subdriver = require("multichannel-device") - return true, subdriver - end - return false -end - local function find_child(device, src_channel) if src_channel == 0 then return device @@ -91,7 +82,7 @@ local multichannel_device = { [MultiChannel.CAPABILITY_REPORT] = capability_get_report_handler } }, - can_handle = can_handle_multichannel_device + can_handle = require("multichannel-device.can_handle") } return multichannel_device \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua new file mode 100644 index 0000000000..846d826c65 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua @@ -0,0 +1,11 @@ +local constants = require "qubino-switches/constants/qubino-constants" + +local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) + if device:id_match(constants.QUBINO_MFR) then + local subdriver = require("qubino-switches") + return true, subdriver + end + return false +end + +return can_handle_qubino_flush_relay \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua new file mode 100644 index 0000000000..47478149b7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua @@ -0,0 +1,9 @@ +return { + {mfr = 0x0159, prod = 0x0001, model = 0x0051, deviceProfile = "qubino-flush-dimmer"}, -- Qubino Flush Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0052, deviceProfile = "qubino-din-dimmer"}, -- Qubino DIN Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0053, deviceProfile = "qubino-flush-dimmer-0-10V"}, -- Qubino Flush Dimmer 0-10V + {mfr = 0x0159, prod = 0x0001, model = 0x0055, deviceProfile = "qubino-mini-dimmer"}, -- Qubino Mini Dimmer + {mfr = 0x0159, prod = 0x0002, model = 0x0051, deviceProfile = "qubino-flush2-relay"}, -- Qubino Flush 2 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0052, deviceProfile = "qubino-flush1-relay"}, -- Qubino Flush 1 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0053, deviceProfile = "qubino-flush1d-relay"} -- Qubino Flush 1D Relay +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua index d883aa4821..576ac6abe8 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua @@ -26,19 +26,11 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=1}) local constants = require "qubino-switches/constants/qubino-constants" -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0001, model = 0x0051, deviceProfile = "qubino-flush-dimmer"}, -- Qubino Flush Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0052, deviceProfile = "qubino-din-dimmer"}, -- Qubino DIN Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0053, deviceProfile = "qubino-flush-dimmer-0-10V"}, -- Qubino Flush Dimmer 0-10V - {mfr = 0x0159, prod = 0x0001, model = 0x0055, deviceProfile = "qubino-mini-dimmer"}, -- Qubino Mini Dimmer - {mfr = 0x0159, prod = 0x0002, model = 0x0051, deviceProfile = "qubino-flush2-relay"}, -- Qubino Flush 2 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0052, deviceProfile = "qubino-flush1-relay"}, -- Qubino Flush 1 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0053, deviceProfile = "qubino-flush1d-relay"} -- Qubino Flush 1D Relay -} +local fingerprints = require("qubino-switches.fingerprints") local function getDeviceProfile(device, isTemperatureSensorOnboard) local newDeviceProfile - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do + for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then newDeviceProfile = fingerprint.deviceProfile if(isTemperatureSensorOnboard) then @@ -51,14 +43,6 @@ local function getDeviceProfile(device, isTemperatureSensorOnboard) return nil end -local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) - if device:id_match(constants.QUBINO_MFR) then - local subdriver = require("qubino-switches") - return true, subdriver - end - return false -end - local function add_temperature_sensor_if_needed(device) if not (device:supports_capability_by_id(capabilities.temperatureMeasurement.ID)) then local new_profile = getDeviceProfile(device, true) @@ -112,7 +96,7 @@ end local qubino_relays = { NAME = "Qubino Relays", - can_handle = can_handle_qubino_flush_relay, + can_handle = require("qubino-switches.can_handle"), zwave_handlers = { [cc.SENSOR_MULTILEVEL] = { [SensorMultilevel.REPORT] = sensor_multilevel_report @@ -126,10 +110,7 @@ local qubino_relays = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("qubino-switches/qubino-relays"), - require("qubino-switches/qubino-dimmer") - } + sub_drivers = require("qubino-switches.sub_drivers"), } return qubino_relays diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua new file mode 100644 index 0000000000..bebc356dd0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua @@ -0,0 +1,12 @@ +local fingerprints = require("qubino-switches.qubino-dimmer.fingerprints") + +local function can_handle_qubino_dimmer(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-switches.qubino-dimmer") + end + end + return false +end + +return can_handle_qubino_dimmer \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua new file mode 100644 index 0000000000..d8b70b9644 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua @@ -0,0 +1,6 @@ +return { + {mfr = 0x0159, prod = 0x0001, model = 0x0051}, -- Qubino Flush Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer + {mfr = 0x0159, prod = 0x0001, model = 0x0053}, -- Qubino Flush Dimmer 0-10V + {mfr = 0x0159, prod = 0x0001, model = 0x0055} -- Qubino Mini Dimmer +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua index 03023adffa..bfb8b090a6 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua @@ -14,22 +14,6 @@ local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local QUBINO_DIMMER_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0001, model = 0x0051}, -- Qubino Flush Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer - {mfr = 0x0159, prod = 0x0001, model = 0x0053}, -- Qubino Flush Dimmer 0-10V - {mfr = 0x0159, prod = 0x0001, model = 0x0055} -- Qubino Mini Dimmer -} - -local function can_handle_qubino_dimmer(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_DIMMER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local function do_configure(self, device) device:send(MultichannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) device:send(MultichannelAssociation:Set({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) @@ -40,10 +24,8 @@ local qubino_dimmer = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_dimmer, - sub_drivers = { - require("qubino-switches/qubino-dimmer/qubino-din-dimmer") - } + can_handle = require("qubino-switches.qubino-dimmer.can_handle"), + sub_drivers = require("qubino-switches.qubino-dimmer.sub_drivers"), } return qubino_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua new file mode 100644 index 0000000000..d097e4ccab --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua @@ -0,0 +1,9 @@ +local function can_handle_qubino_din_dimmer(opts, driver, device, ...) + -- Qubino Din Dimmer: mfr = 0x0159, prod = 0x0001, model = 0x0052 + if device:id_match(0x0159, 0x0001, 0x0052) then + return true, require("qubino-switches.qubino-dimmer.qubino-din-dimmer") + end + return false +end + +return can_handle_qubino_din_dimmer \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua index f763c36c34..a8ee28510f 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua @@ -15,14 +15,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local function can_handle_qubino_din_dimmer(opts, driver, device, ...) - -- Qubino Din Dimmer: mfr = 0x0159, prod = 0x0001, model = 0x0052 - if device:id_match(0x0159, 0x0001, 0x0052) then - return true - end - return false -end - local function do_configure(self, device) device:send(MultichannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) device:send(MultichannelAssociation:Set({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) @@ -34,7 +26,7 @@ local qubino_din_dimmer = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_din_dimmer + can_handle = require("qubino-switches.qubino-dimmer.qubino-din-dimmer.can_handle") } return qubino_din_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua new file mode 100644 index 0000000000..71cfec59d0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua @@ -0,0 +1,5 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("qubino-switches.qubino-dimmer.qubino-din-dimmer"), +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua new file mode 100644 index 0000000000..ac89688b16 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua @@ -0,0 +1,12 @@ +local fingerprints = require("qubino-switches.qubino-relays.fingerprints") + +local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-switches.qubino-relays") + end + end + return false +end + +return can_handle_qubino_flush_relay \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua new file mode 100644 index 0000000000..177d76a797 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua @@ -0,0 +1,5 @@ +return { + {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua index b6e3918cfb..aed54ae7a0 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua @@ -14,21 +14,6 @@ local Association = (require "st.zwave.CommandClass.Association")({ version = 2 }) -local QUBINO_FLUSH_RELAY_FINGERPRINT = { - {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay -} - -local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(QUBINO_FLUSH_RELAY_FINGERPRINT) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local function do_configure(self, device) local association_cmd = Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}}) -- This command needs to be sent before creating component @@ -40,12 +25,8 @@ end local qubino_relays = { NAME = "Qubino Relays", - can_handle = can_handle_qubino_flush_relay, - sub_drivers = { - require("qubino-switches/qubino-relays/qubino-flush-2-relay"), - require("qubino-switches/qubino-relays/qubino-flush-1-relay"), - require("qubino-switches/qubino-relays/qubino-flush-1d-relay") - }, + can_handle = require("qubino-switches.qubino-relays.can_handle"), + sub_drivers = require("qubino-switches.qubino-relays.sub_drivers"), lifecycle_handlers = { doConfigure = do_configure }, diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua new file mode 100644 index 0000000000..5ad4fec09c --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua @@ -0,0 +1,11 @@ + +local QUBINO_FLUSH_1_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0052} + +local function can_handle_qubino_flush_1_relay(opts, driver, device, ...) + if device:id_match(QUBINO_FLUSH_1_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-1-relay") + end + return false +end + +return can_handle_qubino_flush_1_relay \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua index 7fef39539b..541f8fff25 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua @@ -17,12 +17,6 @@ local Association = (require "st.zwave.CommandClass.Association")({version=2}) --- @type st.zwave.CommandClass.MultiChannelAssociation local MultiChannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({version=3}) -local QUBINO_FLUSH_1_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0052} - -local function can_handle_qubino_flush_1_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_1_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1_RELAY_FINGERPRINT.model) -end - local function do_configure(self, device) -- Hub automatically adds device to multiChannelAssosciationGroup and this needs to be removed device:send(MultiChannelAssociation:Remove({grouping_identifier = 1, node_ids = {}})) @@ -40,7 +34,7 @@ local qubino_flush_1_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_flush_1_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-1-relay.can_handle") } return qubino_flush_1_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua new file mode 100644 index 0000000000..cd8f19b180 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua @@ -0,0 +1,11 @@ + +local QUBINO_FLUSH_1D_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0053} + +local function can_handle_qubino_flush_1d_relay(opts, driver, device, ...) + if device:id_match(QUBINO_FLUSH_1D_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-1d-relay") + end + return false +end + +return can_handle_qubino_flush_1d_relay \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua index 55b900920c..75c3ba6e1b 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua @@ -14,12 +14,6 @@ local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) -local QUBINO_FLUSH_1D_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0053} - -local function can_handle_qubino_flush_1d_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_1D_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_1D_RELAY_FINGERPRINT.model) -end - local function do_configure(self, device) device:send(MultichannelAssociation:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) device:refresh() @@ -30,7 +24,7 @@ local qubino_flush_1d_relay = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_qubino_flush_1d_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-1d-relay.can_handle") } return qubino_flush_1d_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua new file mode 100644 index 0000000000..a98f25cc23 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua @@ -0,0 +1,11 @@ +local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } + +local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) + + if device:id_match(QUBINO_FLUSH_2_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_2_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_2_RELAY_FINGERPRINT.model) then + return true, require("qubino-switches.qubino-relays.qubino-flush-2-relay") + end + return false +end + +return can_handle_qubino_flush_2_relay \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua index c0a8d0c967..8bb871a3f6 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua @@ -30,12 +30,6 @@ local utils = require "st.utils" local CHILD_SWITCH_EP = 2 local CHILD_TEMP_SENSOR_EP = 3 -local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } - -local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) - return device:id_match(QUBINO_FLUSH_2_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_2_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_2_RELAY_FINGERPRINT.model) -end - local function component_to_endpoint(device, component_id) return { 1 } end @@ -178,7 +172,7 @@ local qubino_flush_2_relay = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_qubino_flush_2_relay + can_handle = require("qubino-switches.qubino-relays.qubino-flush-2-relay.can_handle") } return qubino_flush_2_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua new file mode 100644 index 0000000000..91e1533f79 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua @@ -0,0 +1,6 @@ +local lazy_load = require "lazy_load_subdriver" +return { + lazy_load("qubino-switches.qubino-relays.qubino-flush-1-relay"), + lazy_load("qubino-switches.qubino-relays.qubino-flush-1d-relay"), + lazy_load("qubino-switches.qubino-relays.qubino-flush-2-relay"), +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua new file mode 100644 index 0000000000..07b46704ee --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua @@ -0,0 +1,6 @@ +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("qubino-switches.qubino-relays"), + lazy_load("qubino-switches.qubino-dimmer"), +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua new file mode 100644 index 0000000000..4e2731384b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("zooz-power-strip.fingerprints") + +local function can_handle_zooz_power_strip(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zooz-power-strip") + return true, subdriver + end + end + return false +end + +return can_handle_zooz_power_strip \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua new file mode 100644 index 0000000000..8a24bd61ee --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua @@ -0,0 +1,3 @@ +return { + {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip +} diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua index 24b969d13e..2f3bc3243b 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua @@ -20,20 +20,6 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local ZOOZ_POWER_STRIP_FINGERPRINTS = { - {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip -} - -local function can_handle_zooz_power_strip(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_POWER_STRIP_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zooz-power-strip") - return true, subdriver - end - end - return false -end - local function binary_event_helper(driver, device, cmd) if cmd.src_channel > 0 then local value = cmd.args.value and cmd.args.value or cmd.args.target_value @@ -124,7 +110,7 @@ local zooz_power_strip = { [capabilities.switch.commands.off.NAME] = switch_off_handler } }, - can_handle = can_handle_zooz_power_strip, + can_handle = require("zooz-power-strip.can_handle"), } return zooz_power_strip diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua new file mode 100644 index 0000000000..15e3bd0b74 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("zooz-zen-30-dimmer-relay.fingerprints") + +local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zooz-zen-30-dimmer-relay") + return true, subdriver + end + end + return false +end + +return can_handle_zooz_zen_30_dimmer_relay_double_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua new file mode 100644 index 0000000000..214a203ab8 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua @@ -0,0 +1,3 @@ +return { + { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch +} diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua index c92c485e43..88e24f5921 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua @@ -70,20 +70,6 @@ local map_key_attribute_to_capability = { } } -local ZOOZ_ZEN_30_DIMMER_RELAY_FINGERPRINTS = { - { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch -} - -local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_ZEN_30_DIMMER_RELAY_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zooz-zen-30-dimmer-relay") - return true, subdriver - end - end - return false -end - local function find_child(parent, src_channel) if src_channel == 0 then return parent @@ -205,7 +191,7 @@ local zooz_zen_30_dimmer_relay_double_switch = { init = device_init, added = device_added }, - can_handle = can_handle_zooz_zen_30_dimmer_relay_double_switch + can_handle = require("zooz-zen-30-dimmer-relay.can_handle") } return zooz_zen_30_dimmer_relay_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua new file mode 100644 index 0000000000..8007b11045 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua @@ -0,0 +1,13 @@ +local fingerprints = require("zwave-dual-switch.fingerprints") + +local function can_handle_zwave_dual_switch(opts, driver, device, ...) + for _, fingerprint in ipairs(fingerprints) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zwave-dual-switch") + return true, subdriver + end + end + return false +end + +return can_handle_zwave_dual_switch \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua new file mode 100644 index 0000000000..581b5a472b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua @@ -0,0 +1,11 @@ +return { + { mfr = 0x0086, prod = 0x0103, model = 0x008C }, -- Aeotec Switch 1 + { mfr = 0x0086, prod = 0x0003, model = 0x008C }, -- Aeotec Switch 1 + { mfr = 0x0258, prod = 0x0003, model = 0x008B }, -- NEO Coolcam Switch 1 + { mfr = 0x0258, prod = 0x0003, model = 0x108B }, -- NEO Coolcam Switch 1 + { mfr = 0x0312, prod = 0xC000, model = 0xC004 }, -- EVA Switch 1 + { mfr = 0x0312, prod = 0xFF00, model = 0xFF05 }, -- Minoston Switch 1 + { mfr = 0x0312, prod = 0xC000, model = 0xC007 }, -- Evalogik Switch 1 + { mfr = 0x010F, prod = 0x1B01, model = 0x1000 }, -- Fibaro Walli Double Switch + { mfr = 0x027A, prod = 0xA000, model = 0xA003 } -- Zooz Double Plug +} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua index 60bf54e4e0..a04f275b50 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua @@ -26,28 +26,6 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) local dualSwitchConfigurationsMap = require "zwave-dual-switch/dual_switch_configurations" local utils = require "st.utils" -local ZWAVE_DUAL_SWITCH_FINGERPRINTS = { - { mfr = 0x0086, prod = 0x0103, model = 0x008C }, -- Aeotec Switch 1 - { mfr = 0x0086, prod = 0x0003, model = 0x008C }, -- Aeotec Switch 1 - { mfr = 0x0258, prod = 0x0003, model = 0x008B }, -- NEO Coolcam Switch 1 - { mfr = 0x0258, prod = 0x0003, model = 0x108B }, -- NEO Coolcam Switch 1 - { mfr = 0x0312, prod = 0xC000, model = 0xC004 }, -- EVA Switch 1 - { mfr = 0x0312, prod = 0xFF00, model = 0xFF05 }, -- Minoston Switch 1 - { mfr = 0x0312, prod = 0xC000, model = 0xC007 }, -- Evalogik Switch 1 - { mfr = 0x010F, prod = 0x1B01, model = 0x1000 }, -- Fibaro Walli Double Switch - { mfr = 0x027A, prod = 0xA000, model = 0xA003 } -- Zooz Double Plug -} - -local function can_handle_zwave_dual_switch(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_DUAL_SWITCH_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zwave-dual-switch") - return true, subdriver - end - end - return false -end - local function find_child(parent, src_channel) if src_channel == 1 then return parent @@ -166,7 +144,7 @@ local zwave_dual_switch = { added = device_added, init = device_init }, - can_handle = can_handle_zwave_dual_switch + can_handle = require("zwave-dual-switch.can_handle") } return zwave_dual_switch From ab076971cd906330ed5078a0e1473e2ade666973 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 4 Nov 2025 15:39:25 -0600 Subject: [PATCH 251/449] Update copyright to 2025 and shorter notice --- adjuster.py | 81 ------------------- .../src/aeon-smart-strip/can_handle.lua | 3 + .../src/aeon-smart-strip/fingerprints.lua | 3 + .../src/aeon-smart-strip/init.lua | 15 +--- .../src/aeotec-heavy-duty/can_handle.lua | 3 + .../src/aeotec-heavy-duty/fingerprints.lua | 3 + .../src/aeotec-heavy-duty/init.lua | 17 +--- .../src/aeotec-smart-switch/can_handle.lua | 3 + .../src/aeotec-smart-switch/fingerprints.lua | 3 + .../src/aeotec-smart-switch/init.lua | 15 +--- .../zwave-switch/src/configurations.lua | 15 +--- .../src/dawon-smart-plug/can_handle.lua | 3 + .../src/dawon-smart-plug/fingerprints.lua | 3 + .../src/dawon-smart-plug/init.lua | 15 +--- .../dawon-wall-smart-switch/can_handle.lua | 3 + .../dawon-wall-smart-switch/fingerprints.lua | 3 + .../src/dawon-wall-smart-switch/init.lua | 15 +--- .../src/eaton-5-scene-keypad/can_handle.lua | 3 + .../src/eaton-5-scene-keypad/fingerprints.lua | 3 + .../src/eaton-5-scene-keypad/init.lua | 15 +--- .../src/eaton-accessory-dimmer/can_handle.lua | 3 + .../eaton-accessory-dimmer/fingerprints.lua | 3 + .../src/eaton-accessory-dimmer/init.lua | 15 +--- .../src/eaton-anyplace-switch/can_handle.lua | 3 + .../eaton-anyplace-switch/fingerprints.lua | 3 + .../src/eaton-anyplace-switch/init.lua | 15 +--- .../src/ecolink-switch/can_handle.lua | 3 + .../src/ecolink-switch/fingerprints.lua | 3 + .../zwave-switch/src/ecolink-switch/init.lua | 15 +--- .../src/fibaro-double-switch/can_handle.lua | 3 + .../src/fibaro-double-switch/fingerprints.lua | 3 + .../src/fibaro-double-switch/init.lua | 15 +--- .../src/fibaro-single-switch/can_handle.lua | 3 + .../src/fibaro-single-switch/fingerprints.lua | 3 + .../src/fibaro-single-switch/init.lua | 15 +--- .../src/fibaro-wall-plug-us/can_handle.lua | 3 + .../src/fibaro-wall-plug-us/fingerprints.lua | 3 + .../src/fibaro-wall-plug-us/init.lua | 15 +--- drivers/SmartThings/zwave-switch/src/init.lua | 15 +--- .../can_handle.lua | 3 + .../fingerprints.lua | 3 + .../inovelli-2-channel-smart-plug/init.lua | 15 +--- .../src/inovelli-LED/can_handle.lua | 3 + .../zwave-switch/src/inovelli-LED/init.lua | 17 +--- .../inovelli-lzw31sn/can_handle.lua | 3 + .../inovelli-LED/inovelli-lzw31sn/init.lua | 15 +--- .../src/inovelli-LED/sub_drivers.lua | 3 + .../zwave-switch/src/lazy_load_subdriver.lua | 3 + .../src/multi-metering-switch/can_handle.lua | 3 + .../multi-metering-switch/fingerprints.lua | 3 + .../src/multi-metering-switch/init.lua | 15 +--- .../multi_metering_switch_configurations.lua | 15 +--- .../src/multichannel-device/can_handle.lua | 3 + .../src/multichannel-device/init.lua | 15 +--- .../zwave-switch/src/preferences.lua | 15 +--- .../src/qubino-switches/can_handle.lua | 3 + .../constants/qubino-constants.lua | 15 +--- .../src/qubino-switches/fingerprints.lua | 3 + .../zwave-switch/src/qubino-switches/init.lua | 15 +--- .../qubino-dimmer/can_handle.lua | 3 + .../qubino-dimmer/fingerprints.lua | 3 + .../qubino-switches/qubino-dimmer/init.lua | 15 +--- .../qubino-din-dimmer/can_handle.lua | 3 + .../qubino-dimmer/qubino-din-dimmer/init.lua | 15 +--- .../qubino-dimmer/sub_drivers.lua | 3 + .../qubino-relays/can_handle.lua | 3 + .../qubino-relays/fingerprints.lua | 3 + .../qubino-switches/qubino-relays/init.lua | 15 +--- .../qubino-flush-1-relay/can_handle.lua | 3 + .../qubino-flush-1-relay/init.lua | 15 +--- .../qubino-flush-1d-relay/can_handle.lua | 3 + .../qubino-flush-1d-relay/init.lua | 15 +--- .../qubino-flush-2-relay/can_handle.lua | 3 + .../qubino-flush-2-relay/init.lua | 15 +--- .../qubino-relays/sub_drivers.lua | 3 + .../src/qubino-switches/sub_drivers.lua | 3 + .../zwave-switch/src/switch_utils.lua | 3 + .../src/test/test_aeon_smart_strip.lua | 15 +--- .../src/test/test_aeotec_dimmer_switch.lua | 15 +--- ..._aeotec_dual_nano_switch_configuration.lua | 15 +--- .../test/test_aeotec_heavy_duty_switch.lua | 15 +--- ...t_aeotec_metering_switch_configuration.lua | 15 +--- .../src/test/test_aeotec_nano_dimmer.lua | 15 +--- .../test_aeotec_nano_dimmer_preferences.lua | 15 +--- .../src/test/test_aeotec_smart_switch.lua | 15 +--- .../test/test_aeotec_smart_switch_7_eu.lua | 15 +--- .../test/test_aeotec_smart_switch_7_us.lua | 15 +--- .../test/test_aeotec_smart_switch_gen5.lua | 15 +--- .../src/test/test_dawon_smart_plug.lua | 15 +--- .../src/test/test_dawon_wall_smart_switch.lua | 15 +--- .../src/test/test_eaton_5_scene_keypad.lua | 15 +--- .../src/test/test_eaton_accessory_dimmer.lua | 15 +--- .../src/test/test_eaton_anyplace_switch.lua | 15 +--- .../src/test/test_eaton_rf_dimmer.lua | 15 +--- .../src/test/test_ecolink_switch.lua | 15 +--- .../src/test/test_fibaro_double_switch.lua | 15 +--- .../src/test/test_fibaro_single_switch.lua | 15 +--- .../src/test/test_fibaro_wall_plug_eu.lua | 15 +--- ...test_fibaro_wall_plug_uk_configuration.lua | 15 +--- .../src/test/test_fibaro_wall_plug_us.lua | 15 +--- .../test_fibaro_walli_dimmer_preferences.lua | 15 +--- .../test/test_fibaro_walli_double_switch.lua | 15 +--- ...fibaro_walli_double_switch_preferences.lua | 15 +--- .../src/test/test_generic_zwave_device1.lua | 15 +--- ...go_control_plug_in_switch_configuraton.lua | 15 +--- .../src/test/test_honeywell_dimmer.lua | 15 +--- .../test_inovelli_2_channel_smart_plug.lua | 15 +--- .../src/test/test_inovelli_button.lua | 15 +--- .../src/test/test_inovelli_dimmer.lua | 15 +--- .../src/test/test_inovelli_dimmer_led.lua | 15 +--- .../test_inovelli_dimmer_power_energy.lua | 15 +--- .../test/test_inovelli_dimmer_preferences.lua | 15 +--- .../src/test/test_inovelli_dimmer_scenes.lua | 15 +--- .../src/test/test_multi_metering_switch.lua | 15 +--- .../src/test/test_multichannel_device.lua | 15 +--- .../test_popp_outdoor_plug_configuration.lua | 15 +--- .../src/test/test_qubino_din_dimmer.lua | 15 +--- .../test_qubino_din_dimmer_preferences.lua | 15 +--- .../test_qubino_flush_1_relay_preferences.lua | 15 +--- ...test_qubino_flush_1d_relay_preferences.lua | 15 +--- .../src/test/test_qubino_flush_2_relay.lua | 15 +--- .../test_qubino_flush_2_relay_preferences.lua | 15 +--- .../src/test/test_qubino_flush_dimmer.lua | 15 +--- ..._qubino_flush_dimmer_0_10V_preferences.lua | 15 +--- .../test_qubino_flush_dimmer_preferences.lua | 15 +--- .../test_qubino_mini_dimmer_preferences.lua | 15 +--- ...t_qubino_temperature_sensor_with_power.lua | 15 +--- ...ubino_temperature_sensor_without_power.lua | 15 +--- .../test_shelly_multi_metering_switch.lua | 15 +--- .../zwave-switch/src/test/test_wyfy_touch.lua | 15 +--- .../test/test_wyfy_touch_configuration.lua | 15 +--- .../src/test/test_zooz_double_plug.lua | 15 +--- .../src/test/test_zooz_power_strip.lua | 15 +--- .../test/test_zooz_zen_30_dimmer_relay.lua | 15 +--- ...t_zooz_zen_30_dimmer_relay_preferences.lua | 15 +--- .../test/test_zwave_dimmer_power_energy.lua | 15 +--- .../src/test/test_zwave_dual_switch.lua | 15 +--- .../test/test_zwave_dual_switch_migration.lua | 15 +--- .../src/test/test_zwave_switch.lua | 15 +--- .../src/test/test_zwave_switch_battery.lua | 15 +--- .../test/test_zwave_switch_electric_meter.lua | 15 +--- .../test/test_zwave_switch_energy_meter.lua | 15 +--- .../src/test/test_zwave_switch_level.lua | 15 +--- .../test/test_zwave_switch_power_meter.lua | 15 +--- .../src/zooz-power-strip/can_handle.lua | 3 + .../src/zooz-power-strip/fingerprints.lua | 3 + .../src/zooz-power-strip/init.lua | 15 +--- .../zooz-zen-30-dimmer-relay/can_handle.lua | 3 + .../zooz-zen-30-dimmer-relay/fingerprints.lua | 3 + .../src/zooz-zen-30-dimmer-relay/init.lua | 15 +--- .../src/zwave-dual-switch/can_handle.lua | 3 + .../dual_switch_configurations.lua | 15 +--- .../src/zwave-dual-switch/fingerprints.lua | 3 + .../src/zwave-dual-switch/init.lua | 15 +--- 154 files changed, 360 insertions(+), 1384 deletions(-) delete mode 100644 adjuster.py diff --git a/adjuster.py b/adjuster.py deleted file mode 100644 index de7f37f587..0000000000 --- a/adjuster.py +++ /dev/null @@ -1,81 +0,0 @@ -import os -import sys -import re - -ignored_folders = ["test"] - -driver = os.getcwd() + "/drivers/SmartThings/zigbee-thermostat/" -subdriver_folders = list(filter(lambda x: "." not in x and x not in ignored_folders, os.listdir(driver + "src/"))) - -for subdriver in subdriver_folders: - path = driver + "src/" + subdriver - init_file = path - - new_init_buffer = [] - new_init = path + "/init.lua.new" - - capture_key = None - captures = {} - - end_counter = 0 - filename = path + "/init.lua" - print(filename) - with open(filename, 'r') as fd: - for line in fd: - - # unnamed can_handle function - if match := re.match(r"(\s*)can_handle\s*=\s*(function.*)",line): - capture_key = "can_handle.lua" - replacement = f"{match.group(1)}can_handle = require(\"{subdriver}.can_handle\")\n" - - print(match.groups()) - print(replacement) - new_init_buffer.append(replacement) - - # named function somewhere else - elif match := re.match(r"(\s*)can_handle\s*=\s*(.*)",line): - replacement = f"{match.group(1)}can_handle = require(\"{subdriver}.can_handle\")\n" - print(match.groups()) - print(replacement) - new_init_buffer.append(replacement) - print("Reread") - - - - - - elif capture_key: - # Create or append the line to the captured segment - try: - captures[capture_key].append(line) - except KeyError: - captures[capture_key] = [line] - print(f"{capture_key}{end_counter}:{line}",end="") - - if match := re.match(r"(?!\s*(?:else|elseif))(?=.*\b(?:then|if|for|function)\b).+$",line): - end_counter += 1 - elif match := re.match(r"\s*end\s*",line): - end_counter = max(end_counter-1,0) - - if end_counter == 0: - capture_key = None - # elif match := re.match(r"\s*local\s+(?:function|(?:(.+\S)\s*=))\s*(?:function)?\s*\(.*\)",line): - # end_counter += 1 - # capture_key = match.group(1) - # captures[capture_key] = [line] - # print(f"{capture_key}:{line}",end="") - # else: - # new_init_buffer.append(line) - - with open(filename, 'r') as fd: - for line in fd: - - for key in captures.keys(): - print(f"Key:{key}") - # for l in captures[key]: - # print(l,end="") - break - # print("="*20) - - # with open(new_init,'w') as fd: - # fd.writelines(new_init_buffer) \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua index 4641be4e95..82e2c1f333 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("aeon-smart-strip.fingerprints") --- Determine whether the passed device is Aeon smart strip diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua index 317928e619..1a1fa044cd 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS } diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua index d957af2042..4bc16ba392 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua index 34ee438a5a..54f67bc438 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("aeotec-heavy-duty.fingerprints") diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua index 726b336997..8728833ae2 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = 0x0086, model = 0x004E } } diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua index 66d1778397..6a650a5ec5 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua @@ -1,18 +1,5 @@ --- Author: CommanderQ --- --- Copyright 2021 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Meter diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua index 907b92e64e..beebb09d05 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("aeotec-smart-switch.fingerprints") local function can_handle(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua index a30a29a55c..ab4cdc0a1a 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0086, prodId = 0x0060}, {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua index e7daab6b86..0b34f1ba85 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) diff --git a/drivers/SmartThings/zwave-switch/src/configurations.lua b/drivers/SmartThings/zwave-switch/src/configurations.lua index b2c283bd7c..199cf03237 100644 --- a/drivers/SmartThings/zwave-switch/src/configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { AEOTEC_METERING_SWITCH = { diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua index ecf5acff5c..f26424710b 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("dawon-smart-plug.fingerprints") --- Determine whether the passed device is Dawon smart plug diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua index 6bf63ecff2..fecf8797b1 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua index da6bbc69af..84643a500f 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua index b8bacfdc4f..f02b728a90 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("dawon-wall-smart-switch.fingerprints") --- Determine whether the passed device is Dawon wall smart switch diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua index 075f7f9edd..ab6633bac4 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x018C, prod = 0x0061, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 1 KR {mfr = 0x018C, prod = 0x0062, model = 0x0001}, -- Dawon Multipurpose Sensor + Smart Switch endpoint 2 KR diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua index 8e5c475321..4f4278c057 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua index 8ff3aab14f..f654e1775b 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("eaton-5-scene-keypad.fingerprints") diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua index 59e9d0d1a0..aca3ef6c99 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad } diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua index 6c92b0895d..f8315135fe 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.capabilities local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua index fd4bfb8b4a..87e8d07284 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("eaton-accessory-dimmer.fingerprints") local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua index 44628d1d0a..623a230db9 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch } \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua index 290cce85e4..bb49665075 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.utils diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua index 33a0471740..ae8532462a 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("eaton-anyplace-switch.fingerprints") local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua index 0a20a7c98e..11bd08aedc 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch } diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua index c965d82ed7..488babc276 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua index 023e6756b3..43ce394a7a 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("ecolink-switch.fingerprints") local function can_handle_ecolink(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua index 1a9122d140..742590dc1d 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x014A, prod = 0x0006, model = 0x0002}, {mfr = 0x014A, prod = 0x0006, model = 0x0003}, diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua index daff734562..081cf113dd 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua index d4c3fae1eb..4d84071cde 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("fibaro-double-switch.fingerprints") local function can_handle_fibaro_double_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua index 50fa9404f3..e6e7a43222 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua index 1cec8ca95d..e02d986b91 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua index b629911db9..c20cd039a6 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("fibaro-single-switch.fingerprints") local function can_handle_fibaro_single_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua index de11fe944f..47b4327905 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua index dce2efda9e..8a3252cc32 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local ButtonDefaults = require "st.zwave.defaults.button" diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua index b3c7f03d8a..c09a58f114 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("fibaro-wall-plug-us.fingerprints") local function can_handle_fibaro_wall_plug(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua index 463f8ce0fe..e354eaec48 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua index 0bb455b294..8800e029ab 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local function component_to_endpoint(device, component_id) if component_id == "main" then diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index be00a73223..5814945884 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.defaults diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua index 429fc4c28f..333f2ab076 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("inovelli-2-channel-smart-plug.fingerprints") local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua index 145020c3df..992af6716a 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x015D, prod = 0x0221, model = 0x251C}, -- Show Home Outlet {mfr = 0x0312, prod = 0x0221, model = 0x251C}, -- Inovelli Outlet diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua index 8f86d4302a..313280d131 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua index a9662f0c12..ac9ae7b229 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local INOVELLI_MANUFACTURER_ID = 0x031E local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua index ffd57c716b..23fafda05d 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.utils @@ -86,7 +75,7 @@ local inovelli_led = { [capabilities.colorControl.commands.setColor.NAME] = set_color } }, - can_handle = require("inovelli-LED.can_handle"), + can_handle = require("inovelli-LED.can_handle"), sub_drivers = require("inovelli-LED.sub_drivers"), } diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua index f7288340d7..26f34df290 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local INOVELLI_MANUFACTURER_ID = 0x031E local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua index 355b4a4fd4..fd0ff5d844 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua index a0d2b99754..212b93b12a 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua index 32ef997530..45115081e4 100644 --- a/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua +++ b/drivers/SmartThings/zwave-switch/src/lazy_load_subdriver.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return function(sub_driver_name) -- gets the current lua libs api version diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua index 036f555362..96d91c2598 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("multi-metering-switch.fingerprints") local function can_handle_multi_metering_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua index 9df75e4fb7..c6d08a4efd 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0086, prod = 0x0003, model = 0x0084}, -- Aeotec Nano Switch 1 {mfr = 0x0086, prod = 0x0103, model = 0x0084}, -- Aeotec Nano Switch 1 diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua index 1d76852411..ca4c8951d9 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local utils = require "st.utils" diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua index dfb773efc1..af047646d5 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/multi_metering_switch_configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { AEOTEC_NANO_SWITCH_1 = { diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua index db4ef36bb9..39c16f1d3d 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local function can_handle_multichannel_device(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua index 48c8b2837d..065de0e968 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local cc = require "st.zwave.CommandClass" local st_device = require "st.device" local MultiChannel = (require "st.zwave.CommandClass.MultiChannel")({ version = 3 }) diff --git a/drivers/SmartThings/zwave-switch/src/preferences.lua b/drivers/SmartThings/zwave-switch/src/preferences.lua index 73ed5e52f3..f0922bc184 100644 --- a/drivers/SmartThings/zwave-switch/src/preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/preferences.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local AEOTEC_HEAVY_DUTY_SWITCH = { PARAMETERS = { diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua index 846d826c65..0b57103666 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local constants = require "qubino-switches/constants/qubino-constants" local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua index 31613f40b9..8395eb78fe 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/constants/qubino-constants.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 return { TEMP_SENSOR_WORK_THRESHOLD = -20, -- Celsius degrees diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua index 47478149b7..18191f9fa1 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0159, prod = 0x0001, model = 0x0051, deviceProfile = "qubino-flush-dimmer"}, -- Qubino Flush Dimmer {mfr = 0x0159, prod = 0x0001, model = 0x0052, deviceProfile = "qubino-din-dimmer"}, -- Qubino DIN Dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua index 576ac6abe8..15d541366a 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua index bebc356dd0..d33f036036 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("qubino-switches.qubino-dimmer.fingerprints") local function can_handle_qubino_dimmer(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua index d8b70b9644..ab2676a923 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0159, prod = 0x0001, model = 0x0051}, -- Qubino Flush Dimmer {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua index bfb8b090a6..d0f0e607a5 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua index d097e4ccab..bfebdbfe9e 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local function can_handle_qubino_din_dimmer(opts, driver, device, ...) -- Qubino Din Dimmer: mfr = 0x0159, prod = 0x0001, model = 0x0052 if device:id_match(0x0159, 0x0001, 0x0052) then diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua index a8ee28510f..e47d4b149f 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua index 71cfec59d0..3e45dd94fb 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua index ac89688b16..62996f4a45 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("qubino-switches.qubino-relays.fingerprints") local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua index 177d76a797..a35ea17c0f 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua index aed54ae7a0..8199235310 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local Association = (require "st.zwave.CommandClass.Association")({ version = 2 }) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua index 5ad4fec09c..eb0bf06e02 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local QUBINO_FLUSH_1_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0052} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua index 541f8fff25..61cf89394e 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Association local Association = (require "st.zwave.CommandClass.Association")({version=2}) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua index cd8f19b180..23ba1e4337 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local QUBINO_FLUSH_1D_RELAY_FINGERPRINT = {mfr = 0x0159, prod = 0x0002, model = 0x0053} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua index 75c3ba6e1b..8803a85af2 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MultichannelAssociation = (require "st.zwave.CommandClass.MultiChannelAssociation")({ version = 3 }) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua index a98f25cc23..cabd180686 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua index 8bb871a3f6..8f60cc1e58 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua index 91e1533f79..27319d943b 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { lazy_load("qubino-switches.qubino-relays.qubino-flush-1-relay"), diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua index 07b46704ee..9ce0702941 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local lazy_load = require "lazy_load_subdriver" return { diff --git a/drivers/SmartThings/zwave-switch/src/switch_utils.lua b/drivers/SmartThings/zwave-switch/src/switch_utils.lua index 2d0d2f4d89..da2e0bb0fb 100644 --- a/drivers/SmartThings/zwave-switch/src/switch_utils.lua +++ b/drivers/SmartThings/zwave-switch/src/switch_utils.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Copyright 2025 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); 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 0985633cdd..f11a379666 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 07e64cffa2..6fb6c9c5ef 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 c5c3331ba6..272321f53f 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 857411c120..23adebd3bb 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d0d87cce8e..2ebeb664bb 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b10fdfe4c9..db1e4c2498 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 dba043703f..a8206a0052 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d5878b6fbc..fdba32d9cb 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 50c1a98a57..079616dba4 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 6e65c5ffab..0ecb71e59d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 a1e3607ce3..4c2b76fdf6 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 543d3b0b97..cbfe5f42a0 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 bfa8073280..ee48f99494 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 5157669f72..d50b7cdaa5 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 2ff605d146..8b5e96b161 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 1661c34f2c..6815a408da 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 d939e78d12..99c6ae4068 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 c208be9849..4f5557f9a3 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 86b830be19..59934e6439 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 cafd5ee79b..0b40d6be33 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 94ccbdb384..58ef03fc3f 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 65636f6c22..44aae0f2fa 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 f0ed1f2520..8aa815e829 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 fc42034137..db69dc9845 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 42144a996a..f4e41b5bee 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 3dbdd66622..2771cbc3a7 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 89056f13e9..309d2c79b0 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 abbce9082b..9b72af072e 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 cde284936a..4125fc6f00 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 12753b2879..bc752ec6f1 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 59fa304000..4391102104 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 250182526d..d7dfe35b24 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 3141e5021f..6df8b97048 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 7605ada316..eab58c99f9 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 6b4e978981..43dd38ee7f 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 2f575d8e06..1202b187e2 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 9373383093..11a79bc12d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 4c7d4c8583..601fbaed0b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 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" 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 20b0eed8f6..165dab059e 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 bd846c93b2..9cec4ecd4e 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 21e7fbba3f..920c6c5b2f 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d9e6b82d63..d44eb8f7cb 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 ba758717e8..3436ccba26 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 8481055813..61a62ef0d7 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 4fc60d1927..3ac1fda4b4 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 77663604d5..cfe8dccde7 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 48fa00fa38..db28991f2d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 cd07b64151..d88d6c39b1 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 a4f60c6a50..f385e0be33 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 6f4a098ef7..ccaf9c7aec 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 83f6674852..c06f516249 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b5606f6ab9..331640b1be 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 cc34fe0737..9bc1387f2b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 0a295fbd91..010607fc2d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 be1b64b05b..abf41946cc 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 d63c644040..477886d76e 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 926733ab34..b3a18291e7 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zw = require "st.zwave" local test = require "integration_test" 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 7ebf45335a..c186f9d36b 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 35f8b341dd..215cc0675d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 4b69b063a7..84502e9835 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 db799e9451..ac396439f6 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 d1d73cd496..effb5845b4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 acba87f120..26a72eb4e6 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 13d3e90a3c..935e4b63e8 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 2a96961755..016cb56ef1 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 b8e834cffe..c8cca92dfb 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zw = require "st.zwave" 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 1905fec5f6..edec7aefba 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua index 4e2731384b..fbc683b4cc 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("zooz-power-strip.fingerprints") local function can_handle_zooz_power_strip(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua index 8a24bd61ee..9925f89af7 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip } diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua index 2f3bc3243b..8bf3ad9dc4 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua index 15e3bd0b74..5fb64fb7f1 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("zooz-zen-30-dimmer-relay.fingerprints") local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua index 214a203ab8..27dce171df 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch } diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua index 88e24f5921..6bb9bd4e1b 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua index 8007b11045..c580e44cb0 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local fingerprints = require("zwave-dual-switch.fingerprints") local function can_handle_zwave_dual_switch(opts, driver, device, ...) diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua index d0b2f83373..f5c01ecf6b 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/dual_switch_configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { FIBARO_WALLI_DOUBLE_SWITCH = { diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua index 581b5a472b..c6d24d8481 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + return { { mfr = 0x0086, prod = 0x0103, model = 0x008C }, -- Aeotec Switch 1 { mfr = 0x0086, prod = 0x0003, model = 0x008C }, -- Aeotec Switch 1 diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua index a04f275b50..4bea42c584 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local st_device = require "st.device" local capabilities = require "st.capabilities" --- @type st.zwave.defaults.switch From c5cae5b6fb7ced332ca0058e8acd392ba4afffdc Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 6 Nov 2025 15:43:27 -0800 Subject: [PATCH 252/449] Merge pull request #2527 from SmartThingsCommunity/new_device/WWSTCERT-8677 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WWSTCERT-8677 Leviton Decora Smart Wi-Fi (2nd Gen) Scene Controller S… --- 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 4654f4a84f..ca4f83b3fe 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -641,6 +641,11 @@ matterManufacturer: vendorId: 0x109B productId: 0x1006 deviceProfileName: light-level-motion + - id: "4251/4103" + deviceLabel: Decora Smart Wi-Fi (2nd Gen) Scene Controller Switch + vendorId: 0x109B + productId: 0x1007 + deviceProfileName: 3-button #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From 8403a399a3fc14d342a71e12060609064758d359 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 6 Nov 2025 15:50:37 -0800 Subject: [PATCH 253/449] Merge pull request #2531 from SmartThingsCommunity/new_device/WWSTCERT-8759 WWSTCERT-8759 OSRAM MATTER CLASSIC A 100W --- 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 ca4f83b3fe..6030dac5d8 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -889,6 +889,16 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0AC5 deviceProfileName: light-color-level + - id: "4489/61442" + deviceLabel: OSRAM MATTER CLASSIC A 100W + vendorId: 0x1189 + productId: 0xF002 + deviceProfileName: light-color-level + - id: "4489/2755" + deviceLabel: SMART MAT A60 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC3 + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 0c93124ac6c33d7329fc29bdc88cc3ca8e399f7a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 7 Nov 2025 11:01:04 -0800 Subject: [PATCH 254/449] Merge pull request #2541 from SmartThingsCommunity/new_device/WWSTCERT-8788 WWSTCERT-8788 NodOn Zigbee Multifunction Relay Switch with Metering --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 236758b30d..a016658b9e 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2385,6 +2385,11 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-2-20 deviceProfileName: switch-level + - id: "NodOn/SIN-4-1-21" + deviceLabel: Zigbee Multifunction Relay Switch with Metering + manufacturer: NodOn + model: SIN-4-1-21 + deviceProfileName: switch-power-energy zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch From af80a9d879d647e4543229129fb5cd5277fe684e Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 10 Nov 2025 13:15:58 -0600 Subject: [PATCH 255/449] Formatting fixes - Newlines at the end of every lua file - moved fingerprints with less than 5 into the can_handle file - Removed a missed old copyright notice - Tested on lua libs release 0.59 (commit ID: 546728a3d24ee825f7a9db89c349abbb22949926) --- .../src/aeon-smart-strip/can_handle.lua | 6 ++++-- .../src/aeon-smart-strip/fingerprints.lua | 6 ------ .../src/aeotec-heavy-duty/can_handle.lua | 9 +++++---- .../src/aeotec-heavy-duty/fingerprints.lua | 6 ------ .../zwave-switch/src/aeotec-heavy-duty/init.lua | 2 +- .../src/aeotec-smart-switch/can_handle.lua | 9 +++++++-- .../src/aeotec-smart-switch/fingerprints.lua | 8 -------- .../src/dawon-smart-plug/can_handle.lua | 7 +++++-- .../src/dawon-smart-plug/fingerprints.lua | 7 ------- .../src/dawon-wall-smart-switch/can_handle.lua | 4 ++-- .../src/eaton-5-scene-keypad/can_handle.lua | 6 ++++-- .../src/eaton-5-scene-keypad/fingerprints.lua | 6 ------ .../src/eaton-accessory-dimmer/can_handle.lua | 6 ++++-- .../src/eaton-accessory-dimmer/fingerprints.lua | 6 ------ .../src/eaton-anyplace-switch/can_handle.lua | 7 +++++-- .../src/eaton-anyplace-switch/fingerprints.lua | 6 ------ .../src/ecolink-switch/can_handle.lua | 4 ++-- .../src/fibaro-double-switch/can_handle.lua | 9 +++++++-- .../src/fibaro-double-switch/fingerprints.lua | 8 -------- .../src/fibaro-single-switch/can_handle.lua | 8 ++++++-- .../src/fibaro-single-switch/fingerprints.lua | 8 -------- .../src/fibaro-wall-plug-us/can_handle.lua | 7 +++++-- .../src/fibaro-wall-plug-us/fingerprints.lua | 7 ------- .../inovelli-2-channel-smart-plug/can_handle.lua | 4 ++-- .../fingerprints.lua | 2 +- .../zwave-switch/src/inovelli-LED/can_handle.lua | 2 +- .../inovelli-LED/inovelli-lzw31sn/can_handle.lua | 2 +- .../src/inovelli-LED/sub_drivers.lua | 2 +- .../src/multi-metering-switch/can_handle.lua | 4 ++-- .../src/multi-metering-switch/fingerprints.lua | 2 +- .../src/multichannel-device/can_handle.lua | 2 +- .../src/multichannel-device/init.lua | 2 +- .../SmartThings/zwave-switch/src/preferences.lua | 2 +- .../src/qubino-switches/can_handle.lua | 4 ++-- .../src/qubino-switches/fingerprints.lua | 2 +- .../qubino-switches/qubino-dimmer/can_handle.lua | 4 ++-- .../qubino-dimmer/fingerprints.lua | 2 +- .../qubino-din-dimmer/can_handle.lua | 2 +- .../qubino-dimmer/sub_drivers.lua | 2 +- .../qubino-switches/qubino-relays/can_handle.lua | 8 ++++++-- .../qubino-relays/fingerprints.lua | 8 -------- .../qubino-flush-1-relay/can_handle.lua | 2 +- .../qubino-flush-1d-relay/can_handle.lua | 2 +- .../qubino-flush-2-relay/can_handle.lua | 3 +-- .../src/qubino-switches/sub_drivers.lua | 2 +- .../zwave-switch/src/switch_utils.lua | 16 +--------------- .../src/test/test_aeotec_heavy_duty_switch.lua | 2 +- .../src/test/test_multichannel_device.lua | 2 +- .../src/zooz-power-strip/can_handle.lua | 6 ++++-- .../src/zooz-power-strip/fingerprints.lua | 6 ------ .../src/zooz-zen-30-dimmer-relay/can_handle.lua | 7 ++++--- .../zooz-zen-30-dimmer-relay/fingerprints.lua | 6 ------ .../src/zwave-dual-switch/can_handle.lua | 4 ++-- .../src/zwave-dual-switch/fingerprints.lua | 2 +- 54 files changed, 101 insertions(+), 167 deletions(-) delete mode 100644 drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua index 82e2c1f333..5efc0d1df0 100644 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/can_handle.lua @@ -1,7 +1,6 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("aeon-smart-strip.fingerprints") --- Determine whether the passed device is Aeon smart strip --- @@ -9,6 +8,9 @@ local fingerprints = require("aeon-smart-strip.fingerprints") --- @param device Device device isntance --- @return boolean true if the device proper, else false local function can_handle_aeon_smart_strip(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("aeon-smart-strip") @@ -19,4 +21,4 @@ local function can_handle_aeon_smart_strip(opts, driver, device, ...) end -return can_handle_aeon_smart_strip \ No newline at end of file +return can_handle_aeon_smart_strip diff --git a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua deleted file mode 100644 index 1a1fa044cd..0000000000 --- a/drivers/SmartThings/zwave-switch/src/aeon-smart-strip/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x0086, prod = 0x0003, model = 0x000B}, -- Aeon Smart Strip DSC11-ZWUS -} diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua index 54f67bc438..7c5995ea76 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/can_handle.lua @@ -1,10 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("aeotec-heavy-duty.fingerprints") - - local function can_handle(opts, driver, device, ...) + local fingerprints = { + { mfr = 0x0086, model = 0x004E } + } + for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, nil, fingerprint.model) then local subdriver = require("aeotec-heavy-duty") @@ -14,4 +15,4 @@ local function can_handle(opts, driver, device, ...) return false end -return can_handle \ No newline at end of file +return can_handle diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua deleted file mode 100644 index 8728833ae2..0000000000 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - { mfr = 0x0086, model = 0x004E } -} diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua index 6a650a5ec5..2a47c4e122 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-heavy-duty/init.lua @@ -104,4 +104,4 @@ local driver_template = { can_handle = require("aeotec-heavy-duty.can_handle") } -return driver_template; \ No newline at end of file +return driver_template; diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua index beebb09d05..b8a5c5587c 100644 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/can_handle.lua @@ -1,9 +1,14 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("aeotec-smart-switch.fingerprints") local function can_handle(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x0086, prodId = 0x0060}, + {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU + {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US + } + for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, nil, fingerprint.prodId) then local subdriver = require("aeotec-smart-switch") @@ -13,4 +18,4 @@ local function can_handle(opts, driver, device, ...) return false end -return can_handle \ No newline at end of file +return can_handle diff --git a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua deleted file mode 100644 index ab4cdc0a1a..0000000000 --- a/drivers/SmartThings/zwave-switch/src/aeotec-smart-switch/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x0086, prodId = 0x0060}, - {mfr = 0x0371, prodId = 0x00AF}, -- Smart Switch 7 EU - {mfr = 0x0371, prodId = 0x0017} -- Smart Switch 7 US -} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua index f26424710b..86fb4d7e68 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/can_handle.lua @@ -1,7 +1,6 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("dawon-smart-plug.fingerprints") --- Determine whether the passed device is Dawon smart plug --- @@ -9,6 +8,10 @@ local fingerprints = require("dawon-smart-plug.fingerprints") --- @param device Device device isntance --- @return boolean true if the device proper, else false local function can_handle_dawon_smart_plug(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug + {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("dawon-smart-plug") @@ -18,4 +21,4 @@ local function can_handle_dawon_smart_plug(opts, driver, device, ...) return false end -return can_handle_dawon_smart_plug \ No newline at end of file +return can_handle_dawon_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua deleted file mode 100644 index fecf8797b1..0000000000 --- a/drivers/SmartThings/zwave-switch/src/dawon-smart-plug/fingerprints.lua +++ /dev/null @@ -1,7 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x018C, prod = 0x0042, model = 0x0005}, -- Dawon Smart Plug - {mfr = 0x018C, prod = 0x0042, model = 0x0008} -- Dawon Smart Multitab -} diff --git a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua index f02b728a90..cad81076d1 100644 --- a/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/dawon-wall-smart-switch/can_handle.lua @@ -1,7 +1,6 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("dawon-wall-smart-switch.fingerprints") --- Determine whether the passed device is Dawon wall smart switch --- @@ -9,6 +8,7 @@ local fingerprints = require("dawon-wall-smart-switch.fingerprints") --- @param device Device device isntance --- @return boolean true if the device proper, else false local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) + local fingerprints = require("dawon-wall-smart-switch.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("dawon-wall-smart-switch") @@ -18,4 +18,4 @@ local function can_handle_dawon_wall_smart_switch(opts, driver, device, ...) return false end -return can_handle_dawon_wall_smart_switch \ No newline at end of file +return can_handle_dawon_wall_smart_switch diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua index f654e1775b..274676cbed 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/can_handle.lua @@ -2,9 +2,11 @@ -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("eaton-5-scene-keypad.fingerprints") local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("eaton-5-scene-keypad") @@ -14,4 +16,4 @@ local function can_handle_eaton_5_scene_keypad(opts, driver, device, ...) return false end -return can_handle_eaton_5_scene_keypad \ No newline at end of file +return can_handle_eaton_5_scene_keypad diff --git a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua deleted file mode 100644 index aca3ef6c99..0000000000 --- a/drivers/SmartThings/zwave-switch/src/eaton-5-scene-keypad/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x001A, prod = 0x574D, model = 0x0000}, -- Eaton 5-Scene Keypad -} diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua index 87e8d07284..6b608b042b 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/can_handle.lua @@ -1,9 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("eaton-accessory-dimmer.fingerprints") local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("eaton-accessory-dimmer") @@ -13,4 +15,4 @@ local function can_handle_eaton_accessory_dimmer(opts, driver, device, ...) return false end -return can_handle_eaton_accessory_dimmer \ No newline at end of file +return can_handle_eaton_accessory_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua deleted file mode 100644 index 623a230db9..0000000000 --- a/drivers/SmartThings/zwave-switch/src/eaton-accessory-dimmer/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x001A, prod = 0x4441, model = 0x0000} -- Eaton Dimmer Switch -} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua index ae8532462a..67aadecb8b 100644 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/can_handle.lua @@ -1,9 +1,12 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("eaton-anyplace-switch.fingerprints") + local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) + local fingerprints = { + { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then local subdriver = require("eaton-anyplace-switch") @@ -13,4 +16,4 @@ local function can_handle_eaton_anyplace_switch(opts, driver, device, ...) return false end -return can_handle_eaton_anyplace_switch \ No newline at end of file +return can_handle_eaton_anyplace_switch diff --git a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua deleted file mode 100644 index 11bd08aedc..0000000000 --- a/drivers/SmartThings/zwave-switch/src/eaton-anyplace-switch/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - { manufacturerId = 0x001A, productType = 0x4243, productId = 0x0000 } -- Eaton Anyplace Switch -} diff --git a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua index 43ce394a7a..87771168f4 100644 --- a/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/ecolink-switch/can_handle.lua @@ -1,9 +1,9 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("ecolink-switch.fingerprints") local function can_handle_ecolink(opts, driver, device, ...) + local fingerprints = require("ecolink-switch.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("ecolink-switch") @@ -13,4 +13,4 @@ local function can_handle_ecolink(opts, driver, device, ...) return false end -return can_handle_ecolink \ No newline at end of file +return can_handle_ecolink diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua index 4d84071cde..5645551e3b 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/can_handle.lua @@ -1,9 +1,14 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("fibaro-double-switch.fingerprints") local function can_handle_fibaro_double_switch(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch + } + for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("fibaro-double-switch") @@ -13,4 +18,4 @@ local function can_handle_fibaro_double_switch(opts, driver, device, ...) return false end -return can_handle_fibaro_double_switch \ No newline at end of file +return can_handle_fibaro_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua deleted file mode 100644 index e6e7a43222..0000000000 --- a/drivers/SmartThings/zwave-switch/src/fibaro-double-switch/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x010F, prod = 0x0203, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0203, model = 0x3000} -- Fibaro Switch -} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua index c20cd039a6..80d41e924b 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/can_handle.lua @@ -1,9 +1,13 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("fibaro-single-switch.fingerprints") local function can_handle_fibaro_single_switch(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch + {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("fibaro-single-switch") @@ -13,4 +17,4 @@ local function can_handle_fibaro_single_switch(opts, driver, device, ...) return false end -return can_handle_fibaro_single_switch \ No newline at end of file +return can_handle_fibaro_single_switch diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua deleted file mode 100644 index 47b4327905..0000000000 --- a/drivers/SmartThings/zwave-switch/src/fibaro-single-switch/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x010F, prod = 0x0403, model = 0x1000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x2000}, -- Fibaro Switch - {mfr = 0x010F, prod = 0x0403, model = 0x3000} -- Fibaro Switch -} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua index c09a58f114..2a4c8f5b40 100644 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/can_handle.lua @@ -1,9 +1,12 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("fibaro-wall-plug-us.fingerprints") local function can_handle_fibaro_wall_plug(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet + {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("fibaro-wall-plug-us") @@ -13,4 +16,4 @@ local function can_handle_fibaro_wall_plug(opts, driver, device, ...) return false end -return can_handle_fibaro_wall_plug \ No newline at end of file +return can_handle_fibaro_wall_plug diff --git a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua deleted file mode 100644 index e354eaec48..0000000000 --- a/drivers/SmartThings/zwave-switch/src/fibaro-wall-plug-us/fingerprints.lua +++ /dev/null @@ -1,7 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x010F, prod = 0x1401, model = 0x1001}, -- Fibaro Outlet - {mfr = 0x010F, prod = 0x1401, model = 0x2000}, -- Fibaro Outlet -} \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua index 333f2ab076..2bc88d57c5 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/can_handle.lua @@ -1,9 +1,9 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("inovelli-2-channel-smart-plug.fingerprints") local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, ...) + local fingerprints = require("inovelli-2-channel-smart-plug.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("inovelli-2-channel-smart-plug") @@ -13,4 +13,4 @@ local function can_handle_inovelli_2_channel_smart_plug(opts, driver, device, .. return false end -return can_handle_inovelli_2_channel_smart_plug \ No newline at end of file +return can_handle_inovelli_2_channel_smart_plug diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua index 992af6716a..ab2150468d 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-2-channel-smart-plug/fingerprints.lua @@ -10,4 +10,4 @@ return { {mfr = 0x015D, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet {mfr = 0x0312, prod = 0x6100, model = 0x6100}, -- Inovelli Outlet {mfr = 0x015D, prod = 0x2500, model = 0x2500}, -- Inovelli Outlet -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua index ac9ae7b229..eed5127205 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua @@ -18,4 +18,4 @@ local function can_handle_inovelli_led(opts, driver, device, ...) return false end -return can_handle_inovelli_led \ No newline at end of file +return can_handle_inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua index 26f34df290..611a8881c2 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua @@ -16,4 +16,4 @@ local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) return false end -return can_handle_inovelli_lzw31sn \ No newline at end of file +return can_handle_inovelli_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua index 212b93b12a..6a51cd350b 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua @@ -5,4 +5,4 @@ local lazy_load = require "lazy_load_subdriver" return { lazy_load("inovelli-LED.inovelli-lzw31sn"), -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua index 96d91c2598..89fd6ef17f 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/can_handle.lua @@ -1,9 +1,9 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("multi-metering-switch.fingerprints") local function can_handle_multi_metering_switch(opts, driver, device, ...) + local fingerprints = require("multi-metering-switch.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("multi-metering-switch") @@ -13,4 +13,4 @@ local function can_handle_multi_metering_switch(opts, driver, device, ...) return false end -return can_handle_multi_metering_switch \ No newline at end of file +return can_handle_multi_metering_switch diff --git a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua index c6d08a4efd..d35e06a5f0 100644 --- a/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/multi-metering-switch/fingerprints.lua @@ -15,4 +15,4 @@ return { {mfr = 0x0460, prod = 0x0002, model = 0x0081}, -- Shelly Wave 2PM {mfr = 0x0460, prod = 0x0002, model = 0x008C}, -- Shelly Wave Pro 2 {mfr = 0x0460, prod = 0x0002, model = 0x008D}, -- Shelly Wave Pro 2PM -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua index 39c16f1d3d..49464a3240 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/can_handle.lua @@ -11,4 +11,4 @@ local function can_handle_multichannel_device(opts, driver, device, ...) return false end -return can_handle_multichannel_device \ No newline at end of file +return can_handle_multichannel_device diff --git a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua index 065de0e968..82cba1330f 100644 --- a/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua +++ b/drivers/SmartThings/zwave-switch/src/multichannel-device/init.lua @@ -74,4 +74,4 @@ local multichannel_device = { can_handle = require("multichannel-device.can_handle") } -return multichannel_device \ No newline at end of file +return multichannel_device diff --git a/drivers/SmartThings/zwave-switch/src/preferences.lua b/drivers/SmartThings/zwave-switch/src/preferences.lua index f0922bc184..af8e8cf772 100644 --- a/drivers/SmartThings/zwave-switch/src/preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/preferences.lua @@ -417,4 +417,4 @@ preferences.to_numeric_value = function(new_value) return numeric end -return preferences \ No newline at end of file +return preferences diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua index 0b57103666..3312c13838 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/can_handle.lua @@ -1,7 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local constants = require "qubino-switches/constants/qubino-constants" +local constants = require "qubino-switches.constants.qubino-constants" local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) if device:id_match(constants.QUBINO_MFR) then @@ -11,4 +11,4 @@ local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) return false end -return can_handle_qubino_flush_relay \ No newline at end of file +return can_handle_qubino_flush_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua index 18191f9fa1..056b743e8f 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/fingerprints.lua @@ -9,4 +9,4 @@ return { {mfr = 0x0159, prod = 0x0002, model = 0x0051, deviceProfile = "qubino-flush2-relay"}, -- Qubino Flush 2 Relay {mfr = 0x0159, prod = 0x0002, model = 0x0052, deviceProfile = "qubino-flush1-relay"}, -- Qubino Flush 1 Relay {mfr = 0x0159, prod = 0x0002, model = 0x0053, deviceProfile = "qubino-flush1d-relay"} -- Qubino Flush 1D Relay -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua index d33f036036..9588fdccae 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/can_handle.lua @@ -1,9 +1,9 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("qubino-switches.qubino-dimmer.fingerprints") local function can_handle_qubino_dimmer(opts, driver, device, ...) + local fingerprints = require("qubino-switches.qubino-dimmer.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then return true, require("qubino-switches.qubino-dimmer") @@ -12,4 +12,4 @@ local function can_handle_qubino_dimmer(opts, driver, device, ...) return false end -return can_handle_qubino_dimmer \ No newline at end of file +return can_handle_qubino_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua index ab2676a923..32946fe973 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/fingerprints.lua @@ -6,4 +6,4 @@ return { {mfr = 0x0159, prod = 0x0001, model = 0x0052}, -- Qubino DIN Dimmer {mfr = 0x0159, prod = 0x0001, model = 0x0053}, -- Qubino Flush Dimmer 0-10V {mfr = 0x0159, prod = 0x0001, model = 0x0055} -- Qubino Mini Dimmer -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua index bfebdbfe9e..b0da891a74 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/qubino-din-dimmer/can_handle.lua @@ -9,4 +9,4 @@ local function can_handle_qubino_din_dimmer(opts, driver, device, ...) return false end -return can_handle_qubino_din_dimmer \ No newline at end of file +return can_handle_qubino_din_dimmer diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua index 3e45dd94fb..96da43c3e6 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-dimmer/sub_drivers.lua @@ -5,4 +5,4 @@ local lazy_load = require "lazy_load_subdriver" return { lazy_load("qubino-switches.qubino-dimmer.qubino-din-dimmer"), -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua index 62996f4a45..4eb7c04595 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/can_handle.lua @@ -1,9 +1,13 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("qubino-switches.qubino-relays.fingerprints") local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) + local fingerprints = { + {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay + {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then return true, require("qubino-switches.qubino-relays") @@ -12,4 +16,4 @@ local function can_handle_qubino_flush_relay(opts, driver, device, cmd, ...) return false end -return can_handle_qubino_flush_relay \ No newline at end of file +return can_handle_qubino_flush_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua deleted file mode 100644 index a35ea17c0f..0000000000 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x0159, prod = 0x0002, model = 0x0051}, -- Qubino Flush 2 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0052}, -- Qubino Flush 1 Relay - {mfr = 0x0159, prod = 0x0002, model = 0x0053} -- Qubino Flush 1D Relay -} diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua index eb0bf06e02..f91589bce9 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1-relay/can_handle.lua @@ -11,4 +11,4 @@ local function can_handle_qubino_flush_1_relay(opts, driver, device, ...) return false end -return can_handle_qubino_flush_1_relay \ No newline at end of file +return can_handle_qubino_flush_1_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua index 23ba1e4337..9d5f69bb34 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-1d-relay/can_handle.lua @@ -11,4 +11,4 @@ local function can_handle_qubino_flush_1d_relay(opts, driver, device, ...) return false end -return can_handle_qubino_flush_1d_relay \ No newline at end of file +return can_handle_qubino_flush_1d_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua index cabd180686..f3cb5f83e9 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/qubino-relays/qubino-flush-2-relay/can_handle.lua @@ -4,11 +4,10 @@ local QUBINO_FLUSH_2_RELAY_FINGERPRINT = { mfr = 0x0159, prod = 0x0002, model = 0x0051 } local function can_handle_qubino_flush_2_relay(opts, driver, device, ...) - if device:id_match(QUBINO_FLUSH_2_RELAY_FINGERPRINT.mfr, QUBINO_FLUSH_2_RELAY_FINGERPRINT.prod, QUBINO_FLUSH_2_RELAY_FINGERPRINT.model) then return true, require("qubino-switches.qubino-relays.qubino-flush-2-relay") end return false end -return can_handle_qubino_flush_2_relay \ No newline at end of file +return can_handle_qubino_flush_2_relay diff --git a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua index 9ce0702941..09a60bf335 100644 --- a/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/qubino-switches/sub_drivers.lua @@ -6,4 +6,4 @@ local lazy_load = require "lazy_load_subdriver" return { lazy_load("qubino-switches.qubino-relays"), lazy_load("qubino-switches.qubino-dimmer"), -} \ No newline at end of file +} diff --git a/drivers/SmartThings/zwave-switch/src/switch_utils.lua b/drivers/SmartThings/zwave-switch/src/switch_utils.lua index da2e0bb0fb..66ad4715f9 100644 --- a/drivers/SmartThings/zwave-switch/src/switch_utils.lua +++ b/drivers/SmartThings/zwave-switch/src/switch_utils.lua @@ -1,20 +1,6 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 --- Copyright 2025 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 switch_utils = {} switch_utils.emit_event_if_latest_state_missing = function(device, component, capability, attribute_name, value) @@ -23,4 +9,4 @@ switch_utils.emit_event_if_latest_state_missing = function(device, component, ca end end -return switch_utils \ No newline at end of file +return switch_utils 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 23adebd3bb..a11fe3abcb 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 @@ -468,4 +468,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 601fbaed0b..7f584e3125 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -2514,4 +2514,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua index fbc683b4cc..a55c1120e6 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/can_handle.lua @@ -1,9 +1,11 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("zooz-power-strip.fingerprints") local function can_handle_zooz_power_strip(opts, driver, device, ...) + local fingerprints = { + {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("zooz-power-strip") @@ -13,4 +15,4 @@ local function can_handle_zooz_power_strip(opts, driver, device, ...) return false end -return can_handle_zooz_power_strip \ No newline at end of file +return can_handle_zooz_power_strip diff --git a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua deleted file mode 100644 index 9925f89af7..0000000000 --- a/drivers/SmartThings/zwave-switch/src/zooz-power-strip/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - {mfr = 0x015D, prod = 0x0651, model = 0xF51C} -- Zooz ZEN 20 Power Strip -} diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua index 5fb64fb7f1..19d44bc623 100644 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/can_handle.lua @@ -1,9 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("zooz-zen-30-dimmer-relay.fingerprints") - local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, device, ...) + local fingerprints = { + { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch + } for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("zooz-zen-30-dimmer-relay") @@ -13,4 +14,4 @@ local function can_handle_zooz_zen_30_dimmer_relay_double_switch(opts, driver, d return false end -return can_handle_zooz_zen_30_dimmer_relay_double_switch \ No newline at end of file +return can_handle_zooz_zen_30_dimmer_relay_double_switch diff --git a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua deleted file mode 100644 index 27dce171df..0000000000 --- a/drivers/SmartThings/zwave-switch/src/zooz-zen-30-dimmer-relay/fingerprints.lua +++ /dev/null @@ -1,6 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return { - { mfr = 0x027A, prod = 0xA000, model = 0xA008 } -- Zooz Zen 30 Dimmer Relay Double Switch -} diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua index c580e44cb0..416c3dfa67 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/can_handle.lua @@ -1,9 +1,9 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local fingerprints = require("zwave-dual-switch.fingerprints") local function can_handle_zwave_dual_switch(opts, driver, device, ...) + local fingerprints = require("zwave-dual-switch.fingerprints") for _, fingerprint in ipairs(fingerprints) do if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then local subdriver = require("zwave-dual-switch") @@ -13,4 +13,4 @@ local function can_handle_zwave_dual_switch(opts, driver, device, ...) return false end -return can_handle_zwave_dual_switch \ No newline at end of file +return can_handle_zwave_dual_switch diff --git a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua index c6d24d8481..9640f5c4d2 100644 --- a/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua +++ b/drivers/SmartThings/zwave-switch/src/zwave-dual-switch/fingerprints.lua @@ -11,4 +11,4 @@ return { { mfr = 0x0312, prod = 0xC000, model = 0xC007 }, -- Evalogik Switch 1 { mfr = 0x010F, prod = 0x1B01, model = 0x1000 }, -- Fibaro Walli Double Switch { mfr = 0x027A, prod = 0xA000, model = 0xA003 } -- Zooz Double Plug -} \ No newline at end of file +} From e5f25aa472201d671cde7571a855323580e67b61 Mon Sep 17 00:00:00 2001 From: Carter Swedal Date: Thu, 6 Nov 2025 13:27:17 -0600 Subject: [PATCH 256/449] Merge pull request #2538 from SmartThingsCommunity/lua_libs_v16_test_fixes Stop accounting for leakage of variables between tests --- .../test_zigbee_window_treatment_hanssem.lua | 7 ++++++ .../src/test/test_tuya_curtain.lua | 10 ++++----- .../tuya-zigbee/src/test/test_tuya_switch.lua | 22 +++++++++---------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index 23814eff28..2e511c99bb 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -88,6 +88,7 @@ end test.register_coroutine_test( "Device Added ", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send( @@ -106,6 +107,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Open handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x00") @@ -147,6 +149,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Close handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -189,6 +192,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Pause handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -243,6 +247,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Set Level handler", function() + SeqNum = 0 test.socket.zigbee:__queue_receive({ mock_device.id, build_rx_message(mock_device,"\x03\x02\x00\x04\x00\x00\x00\x64") @@ -285,6 +290,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Preset position handler", function() + SeqNum = 0 test.socket.capability:__queue_receive( { mock_device.id, @@ -336,6 +342,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Information changed : Reverse", function() + SeqNum = 0 test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") test.socket.zigbee:__queue_receive({ mock_device.id, 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 7a0ea0b215..ca09608347 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -145,7 +145,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 2) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x00', 0) } }, { channel = "capability", @@ -166,7 +166,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 3) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x02', 0) } }, { channel = "capability", @@ -187,7 +187,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 4) } + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 0) } } } ) @@ -203,7 +203,7 @@ test.register_message_test( { channel = "zigbee", 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', 5) } + 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) } } } ) @@ -219,7 +219,7 @@ test.register_message_test( { channel = "zigbee", 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', 6) } + 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) } } } ) 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 4da9d411d7..97537c36ad 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua @@ -252,7 +252,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 1) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -268,7 +268,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 2) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -284,7 +284,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 3) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -300,7 +300,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 4) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -316,7 +316,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 5) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } } ) @@ -332,7 +332,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 6) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -348,7 +348,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 7) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -364,7 +364,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 8) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -380,7 +380,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 9) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -396,7 +396,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 10) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) @@ -412,7 +412,7 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 11) } + message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } } ) From d572842e3cebf9b2759e8caaa906d9cb2626dc6e Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:38:17 -0600 Subject: [PATCH 257/449] default Eve devices to main driver (#2534) --- .../src/sub_drivers/eve_energy/init.lua | 12 +- .../switch_handlers/attribute_handlers.lua | 7 - .../src/test/test_eve_energy.lua | 158 ++++++++++++++++++ 3 files changed, 166 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 6db532934c..1df33b19b0 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -8,9 +8,11 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local cluster_base = require "st.matter.cluster_base" -local utils = require "st.utils" +local st_utils = require "st.utils" local data_types = require "st.matter.data_types" local device_lib = require "st.device" +local switch_utils = require "switch_utils.utils" +local fields = require "switch_utils.fields" local SWITCH_INITIALIZED = "__switch_intialized" local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" @@ -37,9 +39,11 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s ------------------------------------------------------------------------------------- local function is_eve_energy_products(opts, driver, device) - -- this sub driver does not support child devices + -- this sub driver does NOT support child devices, and ONLY supports Eve devices + -- that do NOT support the Electrical Sensor device type if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID then + device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) == 0 then return true end @@ -326,7 +330,7 @@ end local function watt_accumulated_attr_handler(driver, device, ib, zb_rx) if ib.data.value then local totalConsumptionRawValue = ib.data.value - local totalConsumptionWh = utils.round(1000 * totalConsumptionRawValue) + local totalConsumptionWh = st_utils.round(1000 * totalConsumptionRawValue) updateEnergyMeter(device, totalConsumptionWh) report_power_consumption_to_st_energy(device, totalConsumptionWh) end 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 58310ef56b..1ebd114812 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -294,13 +294,6 @@ end function AttributeHandlers.energy_imported_factory(is_cumulative_report) return function(driver, device, ib, response) - -- workaround: ignore devices supporting Eve's private energy cluster AND the ElectricalEnergyMeasurement cluster - local EVE_MANUFACTURER_ID, EVE_PRIVATE_CLUSTER_ID = 0x130A, 0x130AFC01 - local eve_private_energy_eps = device:get_endpoints(EVE_PRIVATE_CLUSTER_ID) - if device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and #eve_private_energy_eps > 0 then - return - end - if is_cumulative_report then AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then 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 c3d7faa4eb..f0ab1b0e9b 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -5,6 +5,7 @@ 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 fields = require "switch_utils.fields" local clusters = require "st.matter.clusters" local cluster_base = require "st.matter.cluster_base" @@ -53,6 +54,73 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_electrical_sensor = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("power-energy-powerConsumption.yml"), + manufacturer_info = { + vendor_id = 0x130A, + product_id = 0x0050, + }, + 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, --u32 bitmap + }, + { + cluster_id = PRIVATE_CLUSTER_ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, --u32 bitmap + } + }, + device_types = { + { device_type_id = 0x010A, device_type_revision = 1 } -- On/Off Plug + } + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.ElectricalEnergyMeasurement.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY, + } + }, + device_types = { + { device_type_id = fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, device_type_revision = 1 } + } + } + } +}) + +local function test_init_electrical_sensor() + local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_electrical_sensor) + for i, cluster in ipairs(cluster_subscribe_list) do + subscribe_request:merge(cluster:subscribe(mock_device_electrical_sensor)) + end + test.socket.matter:__expect_send({ mock_device_electrical_sensor.id, subscribe_request }) + test.mock_device.add_test_device(mock_device_electrical_sensor) +end + local function test_init() local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, @@ -374,4 +442,94 @@ test.register_coroutine_test( } ) +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + local mock_device = mock_device_electrical_sensor + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + test.wait_for_events() + test.mock_time.advance_time(1500) + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end, { test_init = test_init_electrical_sensor } +) + test.run_registered_tests() From f1557c47830f2ddb0071c2f9746d729e0719edf7 Mon Sep 17 00:00:00 2001 From: Trfwww Date: Thu, 13 Nov 2025 04:35:20 +0800 Subject: [PATCH 258/449] Add devices Onvis Smart Plug S4EU (#2516) Signed-off-by: Tian Rf --- drivers/SmartThings/matter-switch/fingerprints.yml | 6 ++++++ tools/localizations/cn.csv | 1 + 2 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 6030dac5d8..fa7c02b79f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -878,6 +878,12 @@ matterManufacturer: vendorId: 0x137F productId: 0x027B deviceProfileName: plug-binary +#Onvis + - id: "5181/4097" + deviceLabel: Onvis Smart Plug S4EU + vendorId: 0x143D + productId: 0x1001 + deviceProfileName: plug-binary #Osram - id: "4489/2564" deviceLabel: OSRAM MATTER PLUG UK diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 44f7589bce..fcf79cb144 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -119,3 +119,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H "HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 "Yanmi Switch (3 Way)",岩米三位智能开关面板 +"Onvis Smart Plug S4EU",Onvis 智能插座S4EU \ No newline at end of file From a53030218cbf27a1b1a68def21e81cba32428d49 Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Thu, 13 Nov 2025 07:27:00 +0900 Subject: [PATCH 259/449] =?UTF-8?q?[Aqara/Wireless=20Remote=20Switch]=20Ad?= =?UTF-8?q?ded=20a=20battery=20capability=20update=20for=20users=20using?= =?UTF-8?q?=20the=20previous=20driver=E2=80=A6=20(#2550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added a battery capability update for users using the previous driver version. * Refine battery percentage calculation function --- .../zigbee-button/src/aqara/init.lua | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index b405b506cd..41dfdbf467 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -70,15 +70,35 @@ local function present_value_attr_handler(driver, device, value, zb_rx) device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) end end + +local function calc_battery_percentage(voltage) + local millivolt = voltage * 100 + local percentage = 0 + if millivolt >= 3000 then + percentage = 100 + elseif millivolt >= 2600 then + local fVoltage = (millivolt * millivolt) * 0.00045; + percentage = fVoltage - 2.277 * millivolt + 2880 + end + + return math.floor(percentage) +end + local function battery_level_handler(driver, device, value, zb_rx) local voltage = value.value local batteryLevel = "normal" + if voltage <= 25 then batteryLevel = "critical" elseif voltage < 28 then batteryLevel = "warning" end - device:emit_event(capabilities.batteryLevel.battery(batteryLevel)) + + if device:supports_capability_by_id(capabilities.battery.ID) then + device:emit_event(capabilities.battery.battery(calc_battery_percentage(voltage))) + elseif device:supports_capability_by_id(capabilities.batteryLevel.ID) then + device:emit_event(capabilities.batteryLevel.battery(batteryLevel)) + end end local is_aqara_products = function(opts, driver, device) From 16cef63ed0b923c00c678eb478157b0b9b8e3b79 Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Fri, 14 Nov 2025 00:39:32 +0100 Subject: [PATCH 260/449] WWSTCERT-8242 Add support to frient heat detector (#2447) * Add support to frient heat detector * removed whitspaces * deleted whitespace * Change the tests to increase code coverage * change the date in test file * Change tests to increase code coverage --- .../zigbee-smoke-detector/fingerprints.yml | 5 + .../profiles/heat-temp-battery-alarm.yml | 55 ++ .../zigbee-smoke-detector/src/frient/init.lua | 34 +- .../zigbee-smoke-detector/src/init.lua | 3 +- .../src/test/test_frient_heat_detector.lua | 583 ++++++++++++++++++ .../src/test/test_frient_smoke_detector.lua | 18 + 6 files changed, 691 insertions(+), 7 deletions(-) create mode 100644 drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua diff --git a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml index 76b105fdb8..ddd1135cbd 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml @@ -34,6 +34,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: SMSZB-120 deviceProfileName: smoke-temp-battery-alarm + - id: "frient/HESZB-120" + deviceLabel: frient Heat Detector + manufacturer: frient A/S + model: HESZB-120 + deviceProfileName: heat-temp-battery-alarm - id: "Heiman/Orvibo/Gas3" deviceLabel: Orvibo Gas Detector manufacturer: Heiman diff --git a/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml b/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml new file mode 100644 index 0000000000..c7abdd4e99 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/profiles/heat-temp-battery-alarm.yml @@ -0,0 +1,55 @@ +name: heat-temp-battery-alarm +components: +- id: main + capabilities: + - id: temperatureAlarm + version: 1 + config: + values: + - key: "temperatureAlarm.value" + enabledValues: + - heat + - cleared + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + - id: alarm + version: 1 + config: + values: + - key: "alarm.value" + enabledValues: + - off + - siren + - key: "{{enumCommands}}" + enabledValues: + - off + - siren + categories: + - name: TempSensor +preferences: + - preferenceId: tempOffset + explicit: true + - name: "tempSensitivity" + title: "Temperature Sensitivity (°C)" + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - name: "warningDuration" + title: "Alarm duration (s)" + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index b882c3012a..1d6d85311a 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -21,6 +21,7 @@ local cluster_base = require "st.zigbee.cluster_base" local Basic = zcl_clusters.Basic local alarm = capabilities.alarm local smokeDetector = capabilities.smokeDetector +local temperatureAlarm = capabilities.temperatureAlarm local IASWD = zcl_clusters.IASWD local IASZone = zcl_clusters.IASZone @@ -72,7 +73,14 @@ end local function device_added(driver, device) device:emit_event(alarm.alarm.off()) - device:emit_event(smokeDetector.smoke.clear()) + + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.cleared()) + end + + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.clear()) + end device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) end @@ -111,14 +119,28 @@ local info_changed = function (driver, device, event, args) end local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message) - if zone_status:is_test_set() then - device:emit_event(smokeDetector.smoke.tested()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.heat()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.tested()) + end elseif zone_status:is_alarm1_set() then - device:emit_event(smokeDetector.smoke.detected()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.heat()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.detected()) + end else device.thread:call_with_delay(6, function () - device:emit_event(smokeDetector.smoke.clear()) + if device:supports_capability(capabilities.temperatureAlarm) then + device:emit_event(temperatureAlarm.temperatureAlarm.cleared()) + end + if device:supports_capability(capabilities.smokeDetector) then + device:emit_event(smokeDetector.smoke.clear()) + end end) end end @@ -249,7 +271,7 @@ local frient_smoke_sensor = { } }, can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "SMSZB-120" + return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SMSZB-120" or device:get_model() == "HESZB-120") end } return frient_smoke_sensor diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index 2508061813..941c3312da 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -22,7 +22,8 @@ local zigbee_smoke_driver_template = { capabilities.smokeDetector, capabilities.battery, capabilities.alarm, - capabilities.temperatureMeasurement + capabilities.temperatureMeasurement, + capabilities.temperatureAlarm }, sub_drivers = { require("frient"), 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 new file mode 100644 index 0000000000..aec09a8435 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua @@ -0,0 +1,583 @@ +-- Copyright 2025 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 IASWD = clusters.IASWD +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local Basic = clusters.Basic +local capabilities = require "st.capabilities" +local alarm = capabilities.alarm +local temperatureAlarm = capabilities.temperatureAlarm +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local POWER_CONFIGURATION_ENDPOINT = 0x23 +local IASZONE_ENDPOINT = 0x23 +local TEMPERATURE_MEASUREMENT_ENDPOINT = 0x26 +local base64 = require "base64" +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local cluster_base = require "st.zigbee.cluster_base" +local defaultWarningDuration = 240 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("heat-temp-battery-alarm.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "HESZB-120", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x23] = { + id = 0x23, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x000f, 0x0020, 0x0500, 0x0502 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0000, 0x0003, 0x0402 } + } + } + } +) + +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_coroutine_test( + "Clear alarm and temperatureAlarm states, and read firmware version when the device is added", function() + test.socket.matter:__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", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", alarm.alarm.off()) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + test.wait_for_events() + 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, + POWER_CONFIGURATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_MEASUREMENT_ENDPOINT + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 100 + ):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + IASZONE_ENDPOINT + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 30, + 300, + 0 + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(IASZONE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, ALARM_DEFAULT_MAX_DURATION) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 24) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(14)) + } + } +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: detected", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + ) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: tested", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0100, 0x01) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + ) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: cleared", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) + }) + + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + + test.wait_for_events() + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" } + } + } + } +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_coroutine_test( + "infochanged to check for necessary preferences settings: tempSensitivity, warningDuration", + function() + local updates = { + preferences = { + tempSensitivity = 1.3, + warningDuration = 100 + } + } + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 600, + 130 + )--:to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, 0x0064)--:to_endpoint(IASZONE_ENDPOINT) + }) + + + test.socket.zigbee:__set_channel_ordering("relaxed") + + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040002", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, "reverse", {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "040005", "PRIMARY_SW_VERSION should be less than '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == "reverse", "SIREN_ENDIAN should be set to 'reverse'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x01) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(defaultWarningDuration), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040005", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "040005", "PRIMARY_SW_VERSION should be greater than or equal to '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == nil, "SIREN_ENDIAN should be set to 'nil'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + expectedConfiguration:set_warning_mode(0x01) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(defaultWarningDuration), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040002", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, "reverse", {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "040005", "PRIMARY_SW_VERSION should be less than '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == "reverse", "SIREN_ENDIAN should be set to 'reverse'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(0x00), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn off the siren", + function() + -- Manually set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "040005", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "040005", "PRIMARY_SW_VERSION should be greater than or equal to '040005'") + assert(mock_device:get_field(SIREN_ENDIAN) == nil, "SIREN_ENDIAN should be set to 'nil'") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + -- Expect the command with reverse endian format + local expectedConfiguration = SirenConfiguration(0x00) + expectedConfiguration:set_warning_mode(0x00) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning(mock_device, + expectedConfiguration, + data_types.Uint16(0x00), + data_types.Uint8(00), + data_types.Enum8(00)) + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Test firmware version conversion using direct simulation", + function() + -- Binary firmware version test cases + local test_cases = { + { + binary = string.char(4, 0, 5), -- \004\000\005 + expected_hex = "040005" -- Expected output + }, + { + binary = string.char(4, 0, 12), -- \004\000\012 + expected_hex = "04000c" -- Expected output + }, + { + binary = string.char(5, 1, 3), -- \005\001\003 + expected_hex = "050103" -- Expected output + } + } + + for i, test_case in ipairs(test_cases) do + print("\n----- Test Case " .. i .. " -----") + local binary_fw = test_case.binary + local expected_hex = test_case.expected_hex + + -- Print the raw binary version and its byte values + print("Binary firmware version (raw):", binary_fw) + print("Binary firmware bytes:", string.format( + "\\%03d\\%03d\\%03d", + string.byte(binary_fw, 1), + string.byte(binary_fw, 2), + string.byte(binary_fw, 3) + )) + + -- Reset the field for clean test + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + + -- Create a mock value object + local mock_value = { + value = binary_fw + } + + -- Simulate what happens in primary_sw_version_attr_handler + local primary_sw_version = mock_value.value:gsub('.', function (c) + return string.format('%02x', string.byte(c)) + end) + + -- Store the version in PRIMARY_SW_VERSION field + mock_device:set_field(PRIMARY_SW_VERSION, primary_sw_version, {persist = true}) + + -- What the conversion should do + print("\nConversion steps:") + local hex_result = "" + for i = 1, #binary_fw do + local char = binary_fw:sub(i, i) + local byte_val = string.byte(char) + local hex_val = string.format('%02x', byte_val) + hex_result = hex_result .. hex_val + print(string.format("Character at position %d: byte value = %d, hex = %02x", + i, byte_val, byte_val)) + end + + print("\nExpected hex result:", expected_hex) + print("Manual conversion result:", hex_result) + + -- Verify the stored version + local stored_version = mock_device:get_field(PRIMARY_SW_VERSION) + print("\nStored version in device field:", stored_version) + assert(stored_version == expected_hex, + string.format("Version mismatch! Expected '%s' but got '%s'", + expected_hex, stored_version or "nil")) + end + end +) + +test.run_registered_tests() \ No newline at end of file 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 ff4fa07184..574f08ae76 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 @@ -258,6 +258,24 @@ test.register_message_test( } ) +test.register_coroutine_test( + "ZoneStatusChangeNotification should be handled: clear", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) + }) + + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.smokeDetector.smoke.clear()) + ) + + test.wait_for_events() + end +) + test.register_message_test( "Temperature report should be handled (C) for the temperature cluster", { From 99161677d180d21c2d5c4d3da801f02cff44af6d Mon Sep 17 00:00:00 2001 From: seojune Date: Fri, 14 Nov 2025 16:06:44 +0900 Subject: [PATCH 261/449] A problem occurred where two events were triggered (on press and on release) when the button was long-pressed, so the implementation was modified to only trigger an event upon a long press. --- .../zigbee-button/src/aqara/init.lua | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 41dfdbf467..c54e34be42 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -57,17 +57,19 @@ local configuration = { } local function present_value_attr_handler(driver, device, value, zb_rx) - local end_point = zb_rx.address_header.src_endpoint.value - local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 - local evt = capabilities.button.button.held({ state_change = true }) - if value.value == 1 then - evt = capabilities.button.button.pushed({ state_change = true }) - elseif value.value == 2 then - evt = capabilities.button.button.double({ state_change = true }) - end - device:emit_event(evt) - if btn_evt_cnt > 1 then - device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) + if value.value < 0xFF then + local end_point = zb_rx.address_header.src_endpoint.value + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local evt = capabilities.button.button.held({ state_change = true }) + if value.value == 1 then + evt = capabilities.button.button.pushed({ state_change = true }) + elseif value.value == 2 then + evt = capabilities.button.button.double({ state_change = true }) + end + device:emit_event(evt) + if btn_evt_cnt > 1 then + device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) + end end end From 165feec0a09eeded950a74c53a0c5e8a04a694ae Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Fri, 14 Nov 2025 13:27:15 -0600 Subject: [PATCH 262/449] sonos: Retry ssdp task creation to fix log spam and spinning --- .../SmartThings/sonos/src/lifecycle_handlers.lua | 1 + drivers/SmartThings/sonos/src/sonos_driver.lua | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua index 97a79ac209..1055d53020 100644 --- a/drivers/SmartThings/sonos/src/lifecycle_handlers.lua +++ b/drivers/SmartThings/sonos/src/lifecycle_handlers.lua @@ -165,6 +165,7 @@ function SonosDriverLifecycleHandlers.initialize_device(driver, device) { hub_logs = true }, string.format("Driver wasn't able to spin up SSDP task, cannot initialize devices.") ) + cosock.socket.sleep(30) end end end, diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 5b9977e50d..1f63ef21d3 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -479,6 +479,17 @@ local function make_ssdp_event_handler( end function SonosDriver:start_ssdp_event_task() + if self.ssdp_task ~= nil then + return + end + cosock.spawn(function () + while self:start_ssdp_event_task_inner() == false do + cosock.socket.sleep(30) + end + end) +end + +function SonosDriver:start_ssdp_event_task_inner() local ssdp_task, err = sonos_ssdp.spawn_persistent_ssdp_task() if err then log.error_with({ hub_logs = true }, string.format("Unable to create SSDP task: %s", err)) @@ -492,7 +503,9 @@ function SonosDriver:start_ssdp_event_task() end self.ssdp_event_thread_handle = cosock.spawn(make_ssdp_event_handler(self, ssdp_task_subscription, oauth_token_subscription)) + return true end + return false end ---@param api_key string From eb90b37a50739f58c4b12188a46265ad458278e9 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:21:34 -0600 Subject: [PATCH 263/449] Add driver support for Matter Cameras (#2503) --- .../matter-switch/fingerprints.yml | 5 + .../matter-switch/profiles/camera.yml | 128 ++ .../SmartThings/matter-switch/src/init.lua | 1 + .../camera_handlers/attribute_handlers.lua | 422 ++++ .../camera_handlers/capability_handlers.lua | 356 ++++ .../camera/camera_handlers/event_handlers.lua | 30 + .../camera_utils/device_configuration.lua | 275 +++ .../camera/camera_utils/fields.lua | 48 + .../sub_drivers/camera/camera_utils/utils.lua | 304 +++ .../src/sub_drivers/camera/can_handle.lua | 16 + .../src/sub_drivers/camera/init.lua | 200 ++ .../matter-switch/src/switch_utils/fields.lua | 17 + .../matter-switch/src/switch_utils/utils.lua | 78 +- .../src/test/test_matter_camera.lua | 1814 +++++++++++++++++ 14 files changed, 3690 insertions(+), 4 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/camera.yml create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index fa7c02b79f..c67eec7bf2 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -3068,6 +3068,11 @@ matterGeneric: - id: 0x0101 # Dimmable Light - id: 0x0107 # Occupancy Sensor deviceProfileName: light-level-motion + - id: "matter/camera" + deviceLabel: Matter Camera + deviceTypes: + - id: 0x0142 # Camera + deviceProfileName: camera matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml new file mode 100644 index 0000000000..2835810ae3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -0,0 +1,128 @@ +name: camera +components: + - id: main + capabilities: + - id: webrtc + version: 1 + optional: true + - id: videoCapture2 + version: 1 + optional: true + - id: videoStreamSettings + version: 1 + optional: true + - id: imageCapture + version: 1 + optional: true + - id: mechanicalPanTiltZoom + version: 1 + optional: true + - id: hdr + version: 1 + optional: true + - id: nightVision + version: 1 + optional: true + - id: imageControl + version: 1 + optional: true + - id: audioRecording + version: 1 + optional: true + - id: sounds + version: 1 + optional: true + - id: cameraPrivacyMode + version: 1 + optional: true + - id: zoneManagement + version: 1 + optional: true + - id: localMediaStorage + version: 1 + optional: true + - id: cameraViewportSettings + version: 1 + optional: true + - id: motionSensor + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Camera + - id: statusLed + optional: true + capabilities: + - id: switch + version: 1 + optional: true + - id: mode + version: 1 + optional: true + - id: speaker + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: microphone + optional: true + capabilities: + - id: audioMute + version: 1 + optional: true + - id: audioVolume + version: 1 + optional: true + - id: doorbell + optional: true + capabilities: + - id: button + version: 1 + optional: true +deviceConfig: + dashboard: + states: + - component: main + capability: imageCapture + version: 1 + values: + - label: "{{___PO_CODE_SAMSUNGELECTRONICS.IM_DEFAULT_IMAGE_CAPTURE}}" + visibleCondition: + component: main + capability: imageCapture + version: 1 + value: captureTime.value + valueType: string + operator: CONTAINS + operand: T + isOffline: false + basicPlus: + - displayType: camera + camera: + image: + component: main + capability: imageCapture + version: 1 + value: image.value + detailView: + - component: main + capability: webrtc + version: 1 + - component: main + capability: mechanicalPanTiltZoom + version: 1 + dpInfo: + - os: ios + dpUri: "storyboard://HMVSController/HMVSViewController" + - os: android + dpUri: "plugin://com.samsung.android.plugin.camera" +metadata: + mnmn: SmartThingsEdge + vid: matter-camera diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 2302ffc4dc..403a14a991 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -296,6 +296,7 @@ local matter_driver_template = { supported_capabilities = fields.supported_capabilities, sub_drivers = { require("sub_drivers.aqara_cube"), + switch_utils.lazy_load_if_possible("sub_drivers.camera"), require("sub_drivers.eve_energy"), require("sub_drivers.third_reality_mk1") } 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 new file mode 100644 index 0000000000..0e3fe5f843 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -0,0 +1,422 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" +local fields = require "switch_utils.fields" +local utils = require "st.utils" + +local CameraAttributeHandlers = {} + +CameraAttributeHandlers.enabled_state_factory = function(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib, attribute(ib.data.value and "enabled" or "disabled")) + if attribute == capabilities.imageControl.imageFlipHorizontal then + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipHorizontal") + elseif attribute == capabilities.imageControl.imageFlipVertical then + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageFlipVertical") + elseif attribute == capabilities.cameraPrivacyMode.hardPrivacyMode then + camera_utils.update_supported_attributes(device, ib, capabilities.cameraPrivacyMode, "hardPrivacyMode") + end + end +end + +CameraAttributeHandlers.night_vision_factory = function(attribute) + return function(driver, device, ib, response) + if camera_fields.tri_state_map[ib.data.value] then + device:emit_event_for_endpoint(ib, attribute(camera_fields.tri_state_map[ib.data.value])) + if attribute == capabilities.nightVision.illumination then + local _ = device:get_latest_state(camera_fields.profile_components.main, capabilities.nightVision.ID, capabilities.nightVision.supportedAttributes.NAME) or + device:emit_event_for_endpoint(ib, capabilities.nightVision.supportedAttributes({"illumination"})) + end + end + end +end + +function CameraAttributeHandlers.image_rotation_handler(driver, device, ib, response) + local degrees = utils.clamp_value(ib.data.value, 0, 359) + device:emit_event_for_endpoint(ib, capabilities.imageControl.imageRotation(degrees)) + camera_utils.update_supported_attributes(device, ib, capabilities.imageControl, "imageRotation") +end + +function CameraAttributeHandlers.two_way_talk_support_handler(driver, device, ib, response) + local two_way_talk_supported = ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX or + ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.FULL_DUPLEX + device:emit_event_for_endpoint(ib, capabilities.webrtc.talkback(two_way_talk_supported)) + if two_way_talk_supported then + device:emit_event_for_endpoint(ib, capabilities.webrtc.talkbackDuplex( + ib.data.value == clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX and "halfDuplex" or "fullDuplex" + )) + end +end + +function CameraAttributeHandlers.muted_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.audioMute.mute(ib.data.value and "muted" or "unmuted")) +end + +function CameraAttributeHandlers.volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) or camera_fields.ABS_VOL_MIN + -- Convert from [min_volume, max_volume] to [0, 100] before emitting capability + local limited_range = max_volume - min_volume + local normalized_volume = utils.round((ib.data.value - min_volume) * 100.0 / limited_range) + device:emit_event_for_endpoint(ib, capabilities.audioVolume.volume(normalized_volume)) +end + +function CameraAttributeHandlers.max_volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local max_volume = ib.data.value + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component) + if max_volume > camera_fields.ABS_VOL_MAX or (min_volume and max_volume <= min_volume) then + device.log.warn(string.format("Device reported invalid maximum (%d) %s volume level range value", ib.data.value, component)) + max_volume = camera_fields.ABS_VOL_MAX + end + device:set_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component, max_volume) +end + +function CameraAttributeHandlers.min_volume_level_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local min_volume = ib.data.value + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. component) + if min_volume < camera_fields.ABS_VOL_MIN or (max_volume and min_volume >= max_volume) then + device.log.warn(string.format("Device reported invalid minimum (%d) %s volume level range value", ib.data.value, component)) + min_volume = camera_fields.ABS_VOL_MIN + end + device:set_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. component, min_volume) +end + +function CameraAttributeHandlers.status_light_enabled_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, ib.data.value and capabilities.switch.switch.on() or capabilities.switch.switch.off()) +end + +function CameraAttributeHandlers.status_light_brightness_handler(driver, device, ib, response) + local component = device:endpoint_to_component(ib) + local _ = device:get_latest_state(component, capabilities.mode.ID, capabilities.mode.supportedModes.NAME) or + device:emit_event_for_endpoint(ib, capabilities.mode.supportedModes({"low", "medium", "high", "auto"}, {visibility = {displayed = false}})) + local _ = device:get_latest_state(component, capabilities.mode.ID, capabilities.mode.supportedArguments.NAME) or + device:emit_event_for_endpoint(ib, capabilities.mode.supportedArguments({"low", "medium", "high", "auto"}, {visibility = {displayed = false}})) + local mode = "auto" + if ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.LOW then + mode = "low" + elseif ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.MEDIUM then + mode = "medium" + elseif ib.data.value == clusters.Global.types.ThreeLevelAutoEnum.HIGH then + mode = "high" + end + device:emit_event_for_endpoint(ib, capabilities.mode.mode(mode)) +end + +function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver, device, ib, response) + if not ib.data.elements then return end + local resolutions = {} + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + local emit_capability = max_encoded_pixel_rate ~= nil and max_fps ~= nil + for _, v in ipairs(ib.data.elements) do + local rate_distortion_trade_off_points = v.elements + local width = rate_distortion_trade_off_points.resolution.elements.width.value + local height = rate_distortion_trade_off_points.resolution.elements.height.value + table.insert(resolutions, { + width = width, + height = height + }) + if emit_capability then + local fps = camera_utils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + if fps > 0 then + resolutions[#resolutions].fps = fps + end + end + end + if emit_capability then + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) + end + device:set_field(camera_fields.SUPPORTED_RESOLUTIONS, resolutions) +end + +function CameraAttributeHandlers.max_encoded_pixel_rate_handler(driver, device, ib, response) + local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + local emit_capability = resolutions ~= nil and max_fps ~= nil + if emit_capability then + for _, v in pairs(resolutions or {}) do + local fps = camera_utils.compute_fps(ib.data.value, v.width, v.height, max_fps) + if fps > 0 then + v.fps = fps + end + end + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) + end + device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) +end + +function CameraAttributeHandlers.video_sensor_parameters_handler(driver, device, ib, response) + if not ib.data.elements then return end + local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local emit_capability = resolutions ~= nil and max_encoded_pixel_rate ~= nil + local sensor_width, sensor_height, max_fps + for _, v in pairs(ib.data.elements) do + if v.field_id == 0 then + sensor_width = v.value + elseif v.field_id == 1 then + sensor_height = v.value + elseif v.field_id == 2 then + max_fps = v.value + end + end + + if max_fps then + if sensor_width and sensor_height then + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ + width = sensor_width, + height = sensor_height, + maxFPS = max_fps + })) + end + if emit_capability then + for _, v in pairs(resolutions or {}) do + local fps = camera_utils.compute_fps(max_encoded_pixel_rate, v.width, v.height, max_fps) + if fps > 0 then + v.fps = fps + end + end + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) + end + device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + end +end + +function CameraAttributeHandlers.min_viewport_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.minViewportResolution({ + width = ib.data.elements.width.value, + height = ib.data.elements.height.value + })) +end + +function CameraAttributeHandlers.allocated_video_streams_handler(driver, device, ib, response) + if not ib.data.elements then return end + local streams = {} + for i, v in ipairs(ib.data.elements) do + local stream = v.elements + local video_stream = { + streamId = stream.video_stream_id.value, + data = { + label = "Stream " .. i, + type = stream.stream_usage.value == clusters.Global.types.StreamUsageEnum.LIVE_VIEW and "liveStream" or "clipRecording", + resolution = { + width = stream.min_resolution.elements.width.value, + height = stream.min_resolution.elements.height.value, + fps = stream.min_frame_rate.value + } + } + } + local viewport = device:get_field(camera_fields.VIEWPORT) + if viewport then + video_stream.data.viewport = viewport + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + video_stream.data.watermark = stream.watermark_enabled.value and "enabled" or "disabled" + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + video_stream.data.onScreenDisplay = stream.osd_enabled.value and "enabled" or "disabled" + end + table.insert(streams, video_stream) + end + if #streams > 0 then + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.videoStreams(streams)) + end +end + +function CameraAttributeHandlers.viewport_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.defaultViewport({ + upperLeftVertex = { x = ib.data.elements.x1.value, y = ib.data.elements.y1.value }, + lowerRightVertex = { x = ib.data.elements.x2.value, y = ib.data.elements.y2.value }, + })) +end + +function CameraAttributeHandlers.ptz_position_handler(driver, device, ib, response) + local ptz_map = camera_utils.get_ptz_map(device) + local emit_event = function(idx, value) + if value ~= ptz_map[idx].current then + device:emit_event_for_endpoint(ib, ptz_map[idx].attribute( + utils.clamp_value(value, ptz_map[idx].range.minimum, ptz_map[idx].range.maximum) + )) + end + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) 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 + 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 + emit_event(camera_fields.ZOOM_IDX, ib.data.elements.zoom.value) + end +end + +function CameraAttributeHandlers.ptz_presets_handler(driver, device, ib, response) + if not ib.data.elements then return end + local presets = {} + 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 + pan = preset.settings.elements.pan.value + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + tilt = preset.settings.elements.tilt.value + end + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) 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 }) + end + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.presets(presets)) +end + +function CameraAttributeHandlers.max_presets_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.maxPresets(ib.data.value)) +end + +function CameraAttributeHandlers.zoom_max_handler(driver, device, ib, response) + if ib.data.value <= camera_fields.ABS_ZOOM_MAX then + device:emit_event_for_endpoint(ib, capabilities.mechanicalPanTiltZoom.zoomRange({ value = { minimum = 1, maximum = ib.data.value } })) + else + device.log.warn(string.format("Device reported invalid maximum zoom (%d)", ib.data.value)) + end +end + +CameraAttributeHandlers.pt_range_handler_factory = function(attribute, limit_field) + return function(driver, device, ib, response) + device:set_field(limit_field, ib.data.value) + local field = string.find(limit_field, "PAN") and "PAN" or "TILT" + local min = device:get_field(camera_fields.pt_range_fields[field].min) + local max = device:get_field(camera_fields.pt_range_fields[field].max) + if min ~= nil and max ~= nil then + local abs_min = field == "PAN" and camera_fields.ABS_PAN_MIN or camera_fields.ABS_TILT_MIN + local abs_max = field == "PAN" and camera_fields.ABS_PAN_MAX or camera_fields.ABS_TILT_MAX + if min < max and min >= abs_min and max <= abs_max then + device:emit_event_for_endpoint(ib, attribute({ value = { minimum = min, maximum = max } })) + device:set_field(camera_fields.pt_range_fields[field].min, nil) + device:set_field(camera_fields.pt_range_fields[field].max, nil) + else + device.log.warn(string.format("Device reported invalid minimum (%d) and maximum (%d) %s " .. + "range values (should be between %d and %d)", min, max, string.lower(field), abs_min, abs_max)) + end + end + end +end + +function CameraAttributeHandlers.max_zones_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.maxZones(ib.data.value)) +end + +function CameraAttributeHandlers.zones_handler(driver, device, ib, response) + if not ib.data.elements then return end + local zones = {} + for _, v in ipairs(ib.data.elements) do + local zone = v.elements + local zone_id = zone.zone_id.value + local zone_type = zone.zone_type.value + local zone_source = zone.zone_source.value + local zone_vertices = {} + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE) and + zone_type == clusters.ZoneManagement.types.ZoneTypeEnum.TWODCART_ZONE then + local zone_name = zone.two_d_cartesian_zone.elements.name.value + local zone_use = zone.two_d_cartesian_zone.elements.use.value + for _, vertex in pairs(zone.two_d_cartesian_zone.elements.vertices.elements or {}) do + table.insert(zone_vertices, {vertex = {x = vertex.elements.x.value, y = vertex.elements.y.value}}) + end + local zone_uses = { + [clusters.ZoneManagement.types.ZoneUseEnum.MOTION] = "motion", + [clusters.ZoneManagement.types.ZoneUseEnum.FOCUS] = "focus", + [clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY] = "privacy" + } + local zone_color = zone.two_d_cartesian_zone.elements.color and zone.two_d_cartesian_zone.elements.color.value or nil + table.insert(zones, { + id = zone_id, + name = zone_name, + type = "2DCartesian", + polygonVertices = zone_vertices, + source = zone_source == clusters.ZoneManagement.types.ZoneSourceEnum.MFG and "manufacturer" or "user", + use = zone_uses[zone_use], + color = zone_color + }) + else + device.log.warn(string.format("Zone type not currently supported: (%s)", zone_type)) + end + end + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.zones({value = zones})) +end + +function CameraAttributeHandlers.triggers_handler(driver, device, ib, response) + if not ib.data.elements then return end + local triggers = {} + for _, v in ipairs(ib.data.elements) do + local trigger = v.elements + table.insert(triggers, { + zoneId = trigger.zone_id.value, + initialDuration = trigger.initial_duration.value, + augmentationDuration = trigger.augmentation_duration.value, + maxDuration = trigger.max_duration.value, + blindDuration = trigger.blind_duration.value, + sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value + }) + end + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggers(triggers)) +end + +function CameraAttributeHandlers.sensitivity_max_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.sensitivityRange({minimum = 1, maximum = ib.data.value}, + {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.sensitivity_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.sensitivity(ib.data.value, {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.installed_chime_sounds_handler(driver, device, ib, response) + if not ib.data.elements then return end + local installed_chimes = {} + for _, v in ipairs(ib.data.elements) do + local chime = v.elements + table.insert(installed_chimes, {id = chime.chime_id.value, label = chime.name.value}) + end + device:emit_event_for_endpoint(ib, capabilities.sounds.supportedSounds(installed_chimes, {visibility = {displayed = false}})) +end + +function CameraAttributeHandlers.selected_chime_handler(driver, device, ib, response) + device:emit_event_for_endpoint(ib, capabilities.sounds.selectedSound(ib.data.value)) +end + +function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response) + if not ib.data.elements then return end + local status_light_enabled_present, status_light_brightness_present = false, false + local attribute_ids, capability_ids = {}, {} + for _, attr in ipairs(ib.data.elements) do + if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then + status_light_enabled_present = true + table.insert(capability_ids, capabilities.switch.ID) + table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) + elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then + status_light_brightness_present = true + table.insert(capability_ids, capabilities.mode.ID) + table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + end + end + local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + component_map.statusLed = { + endpoint_id = ib.endpoint_id, + cluster_id = ib.cluster_id, + attribute_ids = attribute_ids, + capability_ids = capability_ids + } + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) + camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present) +end + +return CameraAttributeHandlers \ No newline at end of file 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 new file mode 100644 index 0000000000..fb85eb863f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -0,0 +1,356 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local utils = require "st.utils" + +local CameraCapabilityHandlers = {} + +CameraCapabilityHandlers.set_enabled_factory = function(attribute) + return function(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(attribute:write(device, endpoint_id, cmd.args.state == "enabled")) + end +end + +CameraCapabilityHandlers.set_night_vision_factory = function(attribute) + return function(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + for i, v in pairs(camera_fields.tri_state_map) do + if v == cmd.args.mode then + device:send(attribute:write(device, endpoint_id, i)) + return + end + end + device.log.warn(string.format("Capability command sent with unknown value: (%s)", cmd.args.mode)) + end +end + +function CameraCapabilityHandlers.handle_set_image_rotation(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local degrees = utils.clamp_value(cmd.args.rotation, 0, 359) + device:send(clusters.CameraAvStreamManagement.attributes.ImageRotation:write(device, endpoint_id, degrees)) +end + +CameraCapabilityHandlers.handle_mute_commands_factory = function(command) + return function(driver, device, cmd) + local attr + if cmd.component == camera_fields.profile_components.speaker then + attr = clusters.CameraAvStreamManagement.attributes.SpeakerMuted + elseif cmd.component == camera_fields.profile_components.microphone then + attr = clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + else + device.log.warn(string.format("Capability command sent from unknown component: (%s)", cmd.component)) + return + end + local endpoint_id = device:component_to_endpoint(cmd.component) + local mute_state = false + if command == capabilities.audioMute.commands.setMute.NAME then + mute_state = cmd.args.state == "muted" + elseif command == capabilities.audioMute.commands.mute.NAME then + mute_state = true + end + device:send(attr:write(device, endpoint_id, mute_state)) + end +end + +function CameraCapabilityHandlers.handle_set_volume(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local volume = utils.round(cmd.args.volume * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, volume)) + else + device.log.warn(string.format("Capability command sent from unknown component: (%s)", cmd.component)) + end +end + +function CameraCapabilityHandlers.handle_volume_up(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) + if not volume or volume >= max_volume then return end + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local converted_volume = utils.round((volume + 1) * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, converted_volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, converted_volume)) + end +end + +function CameraCapabilityHandlers.handle_volume_down(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local max_volume = device:get_field(camera_fields.MAX_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MAX + local min_volume = device:get_field(camera_fields.MIN_VOLUME_LEVEL .. "_" .. cmd.component) or camera_fields.ABS_VOL_MIN + local volume = device:get_latest_state(cmd.component, capabilities.audioVolume.ID, capabilities.audioVolume.volume.NAME) + if not volume or volume <= min_volume then return end + -- Convert from [0, 100] to [min_volume, max_volume] before writing attribute + local volume_range = max_volume - min_volume + local converted_volume = utils.round((volume - 1) * volume_range / 100.0 + min_volume) + if cmd.component == camera_fields.profile_components.speaker then + device:send(clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(device, endpoint_id, converted_volume)) + elseif cmd.component == camera_fields.profile_components.microphone then + device:send(clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(device, endpoint_id, converted_volume)) + end +end + +function CameraCapabilityHandlers.handle_set_status_light_mode(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local level_auto_value + if cmd.args.mode == "low" then level_auto_value = "LOW" + elseif cmd.args.mode == "medium" then level_auto_value = "MEDIUM" + elseif cmd.args.mode == "high" then level_auto_value = "HIGH" + elseif cmd.args.mode == "auto" then level_auto_value = "AUTO" end + if not level_auto_value then + device.log.warn(string.format("Invalid mode received from setMode command: %s", cmd.args.mode)) + return + end + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(device, endpoint_id, + clusters.Global.types.ThreeLevelAutoEnum[level_auto_value])) +end + +function CameraCapabilityHandlers.handle_status_led_on(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, true)) +end + +function CameraCapabilityHandlers.handle_status_led_off(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(device, endpoint_id, false)) +end + +function CameraCapabilityHandlers.handle_audio_recording(driver, device, cmd) + -- TODO: Allocate audio stream if it doesn't exist + local component = device.profile.components[cmd.component] + device:emit_component_event(component, capabilities.audioRecording.audioRecording(cmd.args.state)) +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 + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove( + device, endpoint_id, pan_delta, tilt_delta, zoom_delta + )) + end +end + +CameraCapabilityHandlers.ptz_set_position_factory = function(command) + return function (driver, device, cmd) + local ptz_map = camera_utils.get_ptz_map(device) + 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 + elseif command == capabilities.mechanicalPanTiltZoom.commands.setPan then + ptz_map[camera_fields.PAN_IDX].current = cmd.args.pan + elseif command == capabilities.mechanicalPanTiltZoom.commands.setTilt then + ptz_map[camera_fields.TILT_IDX].current = cmd.args.tilt + 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) + 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 + )) + end +end + +function CameraCapabilityHandlers.handle_save_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSavePreset( + device, endpoint_id, cmd.args.id, cmd.args.label + )) +end + +function CameraCapabilityHandlers.handle_remove_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRemovePreset(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_move_to_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZMoveToPreset(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_new_zone(driver, device, cmd) + local zone_uses = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.FOCUSZONES) and + clusters.ZoneManagement.types.ZoneUseEnum.FOCUS or clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + local vertices = {} + for _, v in pairs(cmd.args.polygonVertices or {}) do + table.insert(vertices, clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = v.value.x, y = v.value.y})) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone( + device, endpoint_id, clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = cmd.args.name, + use = zone_uses[cmd.args.use], + vertices = vertices, + color = cmd.args.color + } + ) + )) +end + +function CameraCapabilityHandlers.handle_update_zone(driver, device, cmd) + local zone_uses = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.FOCUSZONES) and + clusters.ZoneManagement.types.ZoneUseEnum.FOCUS or clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + if not cmd.args.name or not cmd.args.polygonVertices or not cmd.args.use or not cmd.args.color then + local zones = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.zones.NAME + ) or {} + local found_zone = false + for _, v in pairs(zones) do + if v.id == cmd.args.zoneId then + if not cmd.args.name then cmd.args.name = v.name end + if not cmd.args.polygonVertices then cmd.args.polygonVertices = v.polygonVertices end + if not cmd.args.use then cmd.args.use = v.use end + if not cmd.args.color then cmd.args.color = v.color end -- color may be nil, but it is optional in TwoDCartesianZoneStruct + found_zone = true + break + end + end + if not found_zone then + device.log.warn_with({hub_logs = true}, string.format("Zone does not exist, cannot update the zone.")) + return + end + end + local vertices = {} + for _, v in pairs(cmd.args.polygonVertices or {}) do + table.insert(vertices, clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = v.value.x, y = v.value.y})) + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone( + device, endpoint_id, cmd.args.zoneId, clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = cmd.args.name, + use = zone_uses[cmd.args.use], + vertices = vertices, + color = cmd.args.color + } + ) + )) +end + +function CameraCapabilityHandlers.handle_remove_zone(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.RemoveZone(device, endpoint_id, cmd.args.zoneId)) +end + +function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device, cmd) + if not cmd.args.augmentationDuration or not cmd.args.maxDuration or not cmd.args.blindDuration or + (camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and + not cmd.args.sensitivity) then + local triggers = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME + ) or {} + local found_trigger = false + for _, v in pairs(triggers) do + if v.zoneId == cmd.args.zoneId then + if not cmd.args.augmentationDuration then cmd.args.augmentationDuration = v.augmentationDuration end + if not cmd.args.maxDuration then cmd.args.maxDuration = v.maxDuration end + if not cmd.args.blindDuration then cmd.args.blindDuration = v.blindDuration end + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and + not cmd.args.sensitivity then + cmd.args.sensitivity = v.sensitivity + end + found_trigger = true + break + end + end + if not found_trigger then + device.log.warn_with({hub_logs = true}, string.format("Missing fields needed to create trigger.")) + return + end + end + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger( + device, endpoint_id, clusters.ZoneManagement.types.ZoneTriggerControlStruct( + { + zone_id = cmd.args.zoneId, + initial_duration = cmd.args.initialDuration, + augmentation_duration = cmd.args.augmentationDuration, + max_duration = cmd.args.maxDuration, + blind_duration = cmd.args.blindDuration, + sensitivity = cmd.args.sensitivity + } + ) + )) +end + +function CameraCapabilityHandlers.handle_remove_trigger(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.ZoneManagement.server.commands.RemoveTrigger(device, endpoint_id, cmd.args.zoneId)) +end + +function CameraCapabilityHandlers.handle_set_sensitivity(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + if not camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then + device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.id)) + else + device.log.warn(string.format("Can't set global zone sensitivity setting, per zone sensitivity enabled.")) + end +end + +function CameraCapabilityHandlers.handle_play_sound(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.Chime.server.commands.PlayChimeSound(device, endpoint_id)) +end + +function CameraCapabilityHandlers.handle_set_selected_sound(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.Chime.attributes.SelectedChime:write(device, endpoint_id, cmd.args.id)) +end + +function CameraCapabilityHandlers.handle_set_stream(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local watermark_enabled, on_screen_display_enabled + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + watermark_enabled = cmd.args.watermark == "enabled" + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + on_screen_display_enabled = cmd.args.onScreenDisplay == "enabled" + end + device:send(clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(device, endpoint_id, + cmd.args.streamId, watermark_enabled, on_screen_display_enabled + )) +end + +function CameraCapabilityHandlers.handle_set_default_viewport(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:send(clusters.CameraAvStreamManagement.attributes.Viewport:write( + device, endpoint_id, clusters.Global.types.ViewportStruct({ + x1 = cmd.args.upperLeftVertex.x, + x2 = cmd.args.lowerRightVertex.x, + y1 = cmd.args.upperLeftVertex.y, + y2 = cmd.args.lowerRightVertex.y + }) + )) +end + +return CameraCapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua new file mode 100644 index 0000000000..02b63bb37f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/event_handlers.lua @@ -0,0 +1,30 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" + +local CameraEventHandlers = {} + +function CameraEventHandlers.zone_triggered_handler(driver, device, ib, response) + local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} + if not switch_utils.tbl_contains(triggered_zones, ib.data.elements.zone.value) then + table.insert(triggered_zones, {zoneId = ib.data.elements.zone.value}) + device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) + end +end + +function CameraEventHandlers.zone_stopped_handler(driver, device, ib, response) + local triggered_zones = device:get_field(camera_fields.TRIGGERED_ZONES) or {} + for i, v in pairs(triggered_zones) do + if v.zoneId == ib.data.elements.zone.value then + table.remove(triggered_zones, i) + device:set_field(camera_fields.TRIGGERED_ZONES, triggered_zones) + device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggeredZones(triggered_zones)) + end + end +end + +return CameraEventHandlers 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 new file mode 100644 index 0000000000..80ac3be711 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -0,0 +1,275 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local button_cfg = require("switch_utils.device_configuration").ButtonCfg +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local device_cfg = require "switch_utils.device_configuration" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraDeviceConfiguration = {} + +function CameraDeviceConfiguration.create_child_devices(driver, device) + local num_floodlight_eps = 0 + local parent_child_device = false + for _, ep in ipairs(device.endpoints or {}) do + if device:supports_server_cluster(clusters.OnOff.ID, ep.endpoint_id) then + local child_profile = device_cfg.SwitchCfg.assign_profile_for_onoff_ep(device, ep.endpoint_id) + if child_profile then + num_floodlight_eps = num_floodlight_eps + 1 + local name = string.format("%s %d", "Floodlight", num_floodlight_eps) + driver:try_create_device( + { + type = "EDGE_CHILD", + label = name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep.endpoint_id), + vendor_provided_label = name + } + ) + parent_child_device = true + end + end + end + if parent_child_device then + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) + end +end + +function CameraDeviceConfiguration.match_profile(device, status_light_enabled_present, status_light_brightness_present) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local status_led_component_capabilities = {} + local speaker_component_capabilities = {} + local microphone_component_capabilities = {} + local doorbell_component_capabilities = {} + + local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) + if #camera_endpoints > 0 then + local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) + for _, ep_cluster in pairs(camera_ep.clusters or {}) do + if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID then + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(main_component_capabilities, capabilities.videoCapture2.ID) + table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) then + table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.AUDIO) then + table.insert(main_component_capabilities, capabilities.audioRecording.ID) + table.insert(microphone_component_capabilities, capabilities.audioMute.ID) + table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(main_component_capabilities, capabilities.imageCapture.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.PRIVACY) then + table.insert(main_component_capabilities, capabilities.cameraPrivacyMode.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.SPEAKER) then + table.insert(speaker_component_capabilities, capabilities.audioMute.ID) + table.insert(speaker_component_capabilities, capabilities.audioVolume.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL) then + table.insert(main_component_capabilities, capabilities.imageControl.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE) then + table.insert(main_component_capabilities, capabilities.hdr.ID) + end + if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then + table.insert(main_component_capabilities, capabilities.nightVision.ID) + end + elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID then + local clus_has_feature = function(feature_bitmap) + return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) + end + if clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) or + clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then + table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) + end + table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) + elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID then + table.insert(main_component_capabilities, capabilities.zoneManagement.ID) + elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID then + table.insert(main_component_capabilities, capabilities.motionSensor.ID) + elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) + end + end + end + local chime_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CHIME) + if #chime_endpoints > 0 then + table.insert(main_component_capabilities, capabilities.sounds.ID) + end + local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) + if #doorbell_endpoints > 0 then + table.insert(doorbell_component_capabilities, capabilities.button.ID) + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) + button_cfg.configure_buttons(device) + end + if status_light_enabled_present then + table.insert(status_led_component_capabilities, capabilities.switch.ID) + end + if status_light_brightness_present then + table.insert(status_led_component_capabilities, capabilities.mode.ID) + end + + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.main, main_component_capabilities}) + if #status_led_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.statusLed, status_led_component_capabilities}) + end + if #speaker_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.speaker, speaker_component_capabilities}) + end + if #microphone_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.microphone, microphone_component_capabilities}) + end + if #doorbell_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {camera_fields.profile_components.doorbell, doorbell_component_capabilities}) + end + + if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then + device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities}) + end +end + +local function init_webrtc(device) + if device:supports_capability(capabilities.webrtc) then + -- TODO: Check for individual audio/video and talkback features + local transport_provider_ep_ids = device:get_endpoints(clusters.WebRTCTransportProvider.ID) + device:emit_event_for_endpoint(transport_provider_ep_ids[1], capabilities.webrtc.supportedFeatures({ + value = { + bundle = true, + order = "audio/video", + audio = "sendrecv", + video = "recvonly", + turnSource = "player", + supportTrickleICE = true + } + })) + end +end + +local function init_ptz(device) + if device:supports_capability(capabilities.mechanicalPanTiltZoom) then + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) 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 + 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 + 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 + table.insert(supported_attributes, "presets") + table.insert(supported_attributes, "maxPresets") + end + local av_settings_ep_ids = device:get_endpoints(clusters.CameraAvSettingsUserLevelManagement.ID) + device:emit_event_for_endpoint(av_settings_ep_ids[1], capabilities.mechanicalPanTiltZoom.supportedAttributes(supported_attributes)) + end +end + +local function init_zone_management(device) + if device:supports_capability(capabilities.zoneManagement) then + local supported_features = {} + table.insert(supported_features, "triggerAugmentation") + if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then + table.insert(supported_features, "perZoneSensitivity") + end + local zone_management_ep_ids = device:get_endpoints(clusters.ZoneManagement.ID) + device:emit_event_for_endpoint(zone_management_ep_ids[1], capabilities.zoneManagement.supportedFeatures(supported_features)) + end +end + +local function init_local_media_storage(device) + if device:supports_capability(capabilities.localMediaStorage) then + local supported_attributes = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_attributes, "localVideoRecording") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.SNAPSHOT) then + table.insert(supported_attributes, "localSnapshotRecording") + end + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.localMediaStorage.supportedAttributes(supported_attributes)) + end +end + +local function init_audio_recording(device) + if device:supports_capability(capabilities.audioRecording) then + local audio_enabled_state = device:get_latest_state( + camera_fields.profile_components.main, capabilities.audioRecording.ID, capabilities.audioRecording.audioRecording.NAME + ) + if audio_enabled_state == nil then + -- Initialize with enabled default if state is unset + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.audioRecording.audioRecording("enabled")) + end + end +end + +local function init_video_stream_settings(device) + if device:supports_capability(capabilities.videoStreamSettings) then + local supported_features = {} + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + table.insert(supported_features, "liveStreaming") + table.insert(supported_features, "clipRecording") + table.insert(supported_features, "perStreamViewports") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.WATERMARK) then + table.insert(supported_features, "watermark") + end + if camera_utils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY) then + table.insert(supported_features, "onScreenDisplay") + end + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.videoStreamSettings.supportedFeatures(supported_features)) + end +end + +local function init_camera_privacy_mode(device) + if device:supports_capability(capabilities.cameraPrivacyMode) then + local supported_attributes, supported_commands = {}, {} + table.insert(supported_attributes, "softRecordingPrivacyMode") + table.insert(supported_attributes, "softLivestreamPrivacyMode") + table.insert(supported_commands, "setSoftRecordingPrivacyMode") + table.insert(supported_commands, "setSoftLivestreamPrivacyMode") + local av_stream_management_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedAttributes(supported_attributes)) + device:emit_event_for_endpoint(av_stream_management_ep_ids[1], capabilities.cameraPrivacyMode.supportedCommands(supported_commands)) + end +end + +function CameraDeviceConfiguration.initialize_camera_capabilities(device) + init_webrtc(device) + init_ptz(device) + init_zone_management(device) + init_local_media_storage(device) + init_audio_recording(device) + init_video_stream_settings(device) + init_camera_privacy_mode(device) +end + +function CameraDeviceConfiguration.update_doorbell_component_map(device, ep) + local component_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} + component_map.doorbell = ep + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + +return CameraDeviceConfiguration diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua new file mode 100644 index 0000000000..677e2d5dd6 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" + +local CameraFields = {} + +CameraFields.MAX_ENCODED_PIXEL_RATE = "__max_encoded_pixel_rate" +CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" +CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" +CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" +CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" +CameraFields.TRIGGERED_ZONES = "__triggered_zones" +CameraFields.VIEWPORT = "__viewport" + +CameraFields.PAN_IDX = "PAN" +CameraFields.TILT_IDX = "TILT" +CameraFields.ZOOM_IDX = "ZOOM" + +CameraFields.pt_range_fields = { + [CameraFields.PAN_IDX] = { max = "__MAX_PAN" , min = "__MIN_PAN" }, + [CameraFields.TILT_IDX] = { max = "__MAX_TILT" , min = "__MIN_TILT" } +} + +CameraFields.profile_components = { + main = "main", + statusLed = "statusLed", + speaker = "speaker", + microphone = "microphone", + doorbell = "doorbell" +} + +CameraFields.tri_state_map = { + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF] = "off", + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON] = "on", + [clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO] = "auto" +} + +CameraFields.ABS_PAN_MAX = 180 +CameraFields.ABS_PAN_MIN = -180 +CameraFields.ABS_TILT_MAX = 180 +CameraFields.ABS_TILT_MIN = -180 +CameraFields.ABS_ZOOM_MAX = 100 +CameraFields.ABS_ZOOM_MIN = 1 +CameraFields.ABS_VOL_MAX = 254.0 +CameraFields.ABS_VOL_MIN = 0.0 + +return CameraFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua new file mode 100644 index 0000000000..4334d2a304 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -0,0 +1,304 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraUtils = {} + +function CameraUtils.component_to_endpoint(device, component) + local camera_eps = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + table.sort(camera_eps) + for _, ep in ipairs(camera_eps or {}) do + if ep ~= 0 then -- 0 is the matter RootNode endpoint + return ep + end + end + return nil +end + +function CameraUtils.update_camera_component_map(device) + local camera_av_ep_ids = device:get_endpoints(clusters.CameraAvStreamManagement.ID) + if #camera_av_ep_ids > 0 then + -- An assumption here: there is only 1 CameraAvStreamManagement cluster on the device (which is all our profile supports) + local component_map = {} + if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.AUDIO) then + component_map.microphone = { + endpoint_id = camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, + }, + capability_ids = { + capabilities.audioMute.ID, + capabilities.audioVolume.ID, + } + } + end + if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then + component_map.speaker = { + endpoint_id = camera_av_ep_ids[1], + cluster_id = clusters.CameraAvStreamManagement.ID, + attribute_ids = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, + }, + capability_ids = { + capabilities.audioMute.ID, + capabilities.audioVolume.ID, + } + } + end + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) + end +end + +function CameraUtils.get_ptz_map(device) + local mechanicalPanTiltZoom = capabilities.mechanicalPanTiltZoom + local ptz_map = { + [camera_fields.PAN_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.pan.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.panRange.NAME) or + { minimum = camera_fields.ABS_PAN_MIN, maximum = camera_fields.ABS_PAN_MAX }, + attribute = mechanicalPanTiltZoom.pan + }, + [camera_fields.TILT_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.tilt.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.tiltRange.NAME) or + { minimum = camera_fields.ABS_TILT_MIN, maximum = camera_fields.ABS_TILT_MAX }, + attribute = mechanicalPanTiltZoom.tilt + }, + [camera_fields.ZOOM_IDX] = { + current = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.zoom.NAME), + range = device:get_latest_state("main", mechanicalPanTiltZoom.ID, mechanicalPanTiltZoom.zoomRange.NAME) or + { minimum = camera_fields.ABS_ZOOM_MIN, maximum = camera_fields.ABS_ZOOM_MAX }, + attribute = mechanicalPanTiltZoom.zoom + } + } + return ptz_map +end + +function CameraUtils.feature_supported(device, cluster_id, feature_flag) + return #device:get_endpoints(cluster_id, { feature_bitmap = feature_flag }) > 0 +end + +function CameraUtils.update_supported_attributes(device, ib, capability, attribute) + local attribute_set = device:get_latest_state( + camera_fields.profile_components.main, capability.ID, capability.supportedAttributes.NAME + ) or {} + if not switch_utils.tbl_contains(attribute_set, attribute) then + local updated_attribute_set = {} + for _, v in ipairs(attribute_set) do + table.insert(updated_attribute_set, v) + end + table.insert(updated_attribute_set, attribute) + device:emit_event_for_endpoint(ib, capability.supportedAttributes(updated_attribute_set)) + end +end + +function CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + local fps_step = 15.0 + local fps = math.min(max_encoded_pixel_rate / (width * height), max_fps) + return math.tointeger(math.floor(fps / fps_step) * fps_step) +end + +function CameraUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then + return true + end + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return true + end + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then + return true + end + end + end + return false +end + +function CameraUtils.optional_capabilities_list_changed(optional_capabilities, prev_component_list) + local prev_optional_capabilities = {} + for idx, comp in pairs(prev_component_list or {}) do + local cap_list = {} + for _, capability in pairs(comp.capabilities or {}) do + table.insert(cap_list, capability.id) + end + table.insert(prev_optional_capabilities, {idx, cap_list}) + end + if #optional_capabilities ~= #prev_optional_capabilities then + return true + end + for _, capability in pairs(optional_capabilities or {}) do + if not switch_utils.tbl_contains(prev_optional_capabilities, capability) then + return true + end + end + for _, capability in pairs(prev_optional_capabilities or {}) do + if not switch_utils.tbl_contains(optional_capabilities, capability) then + return true + end + end + return false +end + +function CameraUtils.subscribe(device) + local camera_subscribed_attributes = { + [capabilities.hdr.ID] = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation + }, + [capabilities.nightVision.ID] = { + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum + }, + [capabilities.imageControl.ID] = { + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical + }, + [capabilities.cameraPrivacyMode.ID] = { + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn + }, + [capabilities.webrtc.ID] = { + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + 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 + }, + [capabilities.audioMute.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted + }, + [capabilities.audioVolume.ID] = { + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel + }, + [capabilities.mode.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness + }, + [capabilities.switch.ID] = { + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled + }, + [capabilities.videoStreamSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams + }, + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity + }, + [capabilities.sounds.ID] = { + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime + }, + [capabilities.localMediaStorage.ID] = { + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled + }, + [capabilities.cameraViewportSettings.ID] = { + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.Viewport + }, + [capabilities.motionSensor.ID] = { + clusters.OccupancySensing.attributes.Occupancy + } + } + local camera_subscribed_events = { + [capabilities.zoneManagement.ID] = { + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped + }, + [capabilities.button.ID] = { + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete + } + } + + for capability, attr_list in pairs(camera_subscribed_attributes) do + if device:supports_capability_by_id(capability) then + for _, attr in pairs(attr_list) do + device:add_subscribed_attribute(attr) + end + end + end + for capability, event_list in pairs(camera_subscribed_events) do + if device:supports_capability_by_id(capability) then + for _, event in pairs(event_list) do + device:add_subscribed_event(event) + end + end + end + + -- match_profile is called from the CameraAvStreamManagement AttributeList handler, + -- so the subscription needs to be added here first + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 then + device:add_subscribed_attribute(clusters.CameraAvStreamManagement.attributes.AttributeList) + end + + -- Add subscription for attributes specific to child devices + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + for _, ep in ipairs(device.endpoints or {}) do + local id = 0 + for _, dt in ipairs(ep.device_types or {}) do + if dt.device_type_id ~= fields.DEVICE_TYPE_ID.GENERIC_SWITCH then + id = math.max(id, dt.device_type_id) + end + end + for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do + device:add_subscribed_attribute(attr) + end + end + end + + local im = require "st.matter.interaction_model" + local subscribed_attributes = device:get_field("__subscribed_attributes") or {} + local subscribed_events = device:get_field("__subscribed_events") or {} + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + for _, attributes in pairs(subscribed_attributes) do + for _, ib in pairs(attributes) do + subscribe_request:with_info_block(ib) + end + end + for _, events in pairs(subscribed_events) do + for _, ib in pairs(events) do + subscribe_request:with_info_block(ib) + end + end + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end +end + +return CameraUtils diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua new file mode 100644 index 0000000000..25a441d641 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local device_lib = require "st.device" + local fields = require "switch_utils.fields" + local switch_utils = require "switch_utils.utils" + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + local version = require "version" + if version.rpc >= 10 and version.api >= 16 and + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) > 0 then + return true, require("sub_drivers.camera") + end + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua new file mode 100644 index 0000000000..8244f4fd62 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -0,0 +1,200 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +------------------------------------------------------------------------------------- +-- Matter Camera Sub Driver +------------------------------------------------------------------------------------- + +local attribute_handlers = require "sub_drivers.camera.camera_handlers.attribute_handlers" +local button_cfg = require("switch_utils.device_configuration").ButtonCfg +local camera_cfg = require "sub_drivers.camera.camera_utils.device_configuration" +local camera_fields = require "sub_drivers.camera.camera_utils.fields" +local camera_utils = require "sub_drivers.camera.camera_utils.utils" +local capabilities = require "st.capabilities" +local capability_handlers = require "sub_drivers.camera.camera_handlers.capability_handlers" +local clusters = require "st.matter.clusters" +local event_handlers = require "sub_drivers.camera.camera_handlers.event_handlers" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +local CameraLifecycleHandlers = {} + +function CameraLifecycleHandlers.device_init(driver, device) + device:set_component_to_endpoint_fn(camera_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + device:extend_device("emit_event_for_endpoint", switch_utils.emit_event_for_endpoint) + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + device:set_find_child(switch_utils.find_child) + end + device:extend_device("subscribe", camera_utils.subscribe) + device:subscribe() +end + +function CameraLifecycleHandlers.do_configure(driver, device) + camera_utils.update_camera_component_map(device) + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then + camera_cfg.match_profile(device, false, false) + end + camera_cfg.create_child_devices(driver, device) + camera_cfg.initialize_camera_capabilities(device) +end + +function CameraLifecycleHandlers.info_changed(driver, device, event, args) + if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then + camera_cfg.initialize_camera_capabilities(device) + if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then + button_cfg.configure_buttons(device) + end + device:subscribe() + end +end + +function CameraLifecycleHandlers.added() end + +local camera_handler = { + NAME = "Camera Handler", + lifecycle_handlers = { + init = CameraLifecycleHandlers.device_init, + infoChanged = CameraLifecycleHandlers.info_changed, + doConfigure = CameraLifecycleHandlers.do_configure, + driverSwitched = CameraLifecycleHandlers.do_configure, + added = CameraLifecycleHandlers.added + }, + matter_handlers = { + attr = { + [clusters.CameraAvStreamManagement.ID] = { + [clusters.CameraAvStreamManagement.attributes.HDRModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.hdr.hdr), + [clusters.CameraAvStreamManagement.attributes.NightVision.ID] = attribute_handlers.night_vision_factory(capabilities.nightVision.nightVision), + [clusters.CameraAvStreamManagement.attributes.NightVisionIllum.ID] = attribute_handlers.night_vision_factory(capabilities.nightVision.illumination), + [clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipHorizontal), + [clusters.CameraAvStreamManagement.attributes.ImageFlipVertical.ID] = attribute_handlers.enabled_state_factory(capabilities.imageControl.imageFlipVertical), + [clusters.CameraAvStreamManagement.attributes.ImageRotation.ID] = attribute_handlers.image_rotation_handler, + [clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softRecordingPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.softLivestreamPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn.ID] = attribute_handlers.enabled_state_factory(capabilities.cameraPrivacyMode.hardPrivacyMode), + [clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport.ID] = attribute_handlers.two_way_talk_support_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMuted.ID] = attribute_handlers.muted_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel.ID] = attribute_handlers.volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID] = attribute_handlers.max_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID] = attribute_handlers.min_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMuted.ID] = attribute_handlers.muted_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel.ID] = attribute_handlers.volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID] = attribute_handlers.max_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID] = attribute_handlers.min_volume_level_handler, + [clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID] = attribute_handlers.status_light_enabled_handler, + [clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID] = attribute_handlers.status_light_brightness_handler, + [clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints.ID] = attribute_handlers.rate_distortion_trade_off_points_handler, + [clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate.ID] = attribute_handlers.max_encoded_pixel_rate_handler, + [clusters.CameraAvStreamManagement.attributes.VideoSensorParams.ID] = attribute_handlers.video_sensor_parameters_handler, + [clusters.CameraAvStreamManagement.attributes.MinViewportResolution.ID] = attribute_handlers.min_viewport_handler, + [clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams.ID] = attribute_handlers.allocated_video_streams_handler, + [clusters.CameraAvStreamManagement.attributes.Viewport.ID] = attribute_handlers.viewport_handler, + [clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localSnapshotRecording), + [clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled.ID] = attribute_handlers.enabled_state_factory(capabilities.localMediaStorage.localVideoRecording), + [clusters.CameraAvStreamManagement.attributes.AttributeList.ID] = attribute_handlers.camera_av_stream_management_attribute_list_handler + }, + [clusters.CameraAvSettingsUserLevelManagement.ID] = { + [clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition.ID] = attribute_handlers.ptz_position_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets.ID] = attribute_handlers.ptz_presets_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets.ID] = attribute_handlers.max_presets_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax.ID] = attribute_handlers.zoom_max_handler, + [clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.panRange, camera_fields.pt_range_fields[camera_fields.PAN_IDX].max), + [clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.panRange, camera_fields.pt_range_fields[camera_fields.PAN_IDX].min), + [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].max), + [clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin.ID] = attribute_handlers.pt_range_handler_factory(capabilities.mechanicalPanTiltZoom.tiltRange, camera_fields.pt_range_fields[camera_fields.TILT_IDX].min) + }, + [clusters.ZoneManagement.ID] = { + [clusters.ZoneManagement.attributes.MaxZones.ID] = attribute_handlers.max_zones_handler, + [clusters.ZoneManagement.attributes.Zones.ID] = attribute_handlers.zones_handler, + [clusters.ZoneManagement.attributes.Triggers.ID] = attribute_handlers.triggers_handler, + [clusters.ZoneManagement.attributes.SensitivityMax.ID] = attribute_handlers.sensitivity_max_handler, + [clusters.ZoneManagement.attributes.Sensitivity.ID] = attribute_handlers.sensitivity_handler, + }, + [clusters.Chime.ID] = { + [clusters.Chime.attributes.InstalledChimeSounds.ID] = attribute_handlers.installed_chime_sounds_handler, + [clusters.Chime.attributes.SelectedChime.ID] = attribute_handlers.selected_chime_handler + } + }, + event = { + [clusters.ZoneManagement.ID] = { + [clusters.ZoneManagement.events.ZoneTriggered.ID] = event_handlers.zone_triggered_handler, + [clusters.ZoneManagement.events.ZoneStopped.ID] = event_handlers.zone_stopped_handler + } + } + }, + capability_handlers = { + [capabilities.hdr.ID] = { + [capabilities.hdr.commands.setHdr.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.HDRModeEnabled) + }, + [capabilities.nightVision.ID] = { + [capabilities.nightVision.commands.setNightVision.NAME] = capability_handlers.set_night_vision_factory(clusters.CameraAvStreamManagement.attributes.NightVision), + [capabilities.nightVision.commands.setIllumination.NAME] = capability_handlers.set_night_vision_factory(clusters.CameraAvStreamManagement.attributes.NightVisionIllum) + }, + [capabilities.imageControl.ID] = { + [capabilities.imageControl.commands.setImageFlipHorizontal.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal), + [capabilities.imageControl.commands.setImageFlipVertical.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.ImageFlipVertical), + [capabilities.imageControl.commands.setImageRotation.NAME] = capability_handlers.handle_set_image_rotation + }, + [capabilities.cameraPrivacyMode.ID] = { + [capabilities.cameraPrivacyMode.commands.setSoftLivestreamPrivacyMode.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled), + [capabilities.cameraPrivacyMode.commands.setSoftRecordingPrivacyMode.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled) + }, + [capabilities.audioMute.ID] = { + [capabilities.audioMute.commands.setMute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.setMute.NAME), + [capabilities.audioMute.commands.mute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.mute.NAME), + [capabilities.audioMute.commands.unmute.NAME] = capability_handlers.handle_mute_commands_factory(capabilities.audioMute.commands.unmute.NAME) + }, + [capabilities.audioVolume.ID] = { + [capabilities.audioVolume.commands.setVolume.NAME] = capability_handlers.handle_set_volume, + [capabilities.audioVolume.commands.volumeUp.NAME] = capability_handlers.handle_volume_up, + [capabilities.audioVolume.commands.volumeDown.NAME] = capability_handlers.handle_volume_down + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_status_light_mode + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_status_led_on, + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_status_led_off + }, + [capabilities.audioRecording.ID] = { + [capabilities.audioRecording.commands.setAudioRecording.NAME] = capability_handlers.handle_audio_recording + }, + [capabilities.mechanicalPanTiltZoom.ID] = { + [capabilities.mechanicalPanTiltZoom.commands.panRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.PAN_IDX), + [capabilities.mechanicalPanTiltZoom.commands.tiltRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.TILT_IDX), + [capabilities.mechanicalPanTiltZoom.commands.zoomRelative.NAME] = capability_handlers.ptz_relative_move_factory(camera_fields.ZOOM_IDX), + [capabilities.mechanicalPanTiltZoom.commands.setPan.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setPan), + [capabilities.mechanicalPanTiltZoom.commands.setTilt.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setTilt), + [capabilities.mechanicalPanTiltZoom.commands.setZoom.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setZoom), + [capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom.NAME] = capability_handlers.ptz_set_position_factory(capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom), + [capabilities.mechanicalPanTiltZoom.commands.savePreset.NAME] = capability_handlers.handle_save_preset, + [capabilities.mechanicalPanTiltZoom.commands.removePreset.NAME] = capability_handlers.handle_remove_preset, + [capabilities.mechanicalPanTiltZoom.commands.moveToPreset.NAME] = capability_handlers.handle_move_to_preset + }, + [capabilities.zoneManagement.ID] = { + [capabilities.zoneManagement.commands.newZone.NAME] = capability_handlers.handle_new_zone, + [capabilities.zoneManagement.commands.updateZone.NAME] = capability_handlers.handle_update_zone, + [capabilities.zoneManagement.commands.removeZone.NAME] = capability_handlers.handle_remove_zone, + [capabilities.zoneManagement.commands.createOrUpdateTrigger.NAME] = capability_handlers.handle_create_or_update_trigger, + [capabilities.zoneManagement.commands.removeTrigger.NAME] = capability_handlers.handle_remove_trigger, + [capabilities.zoneManagement.commands.setSensitivity.NAME] = capability_handlers.handle_set_sensitivity + }, + [capabilities.sounds.ID] = { + [capabilities.sounds.commands.playSound.NAME] = capability_handlers.handle_play_sound, + [capabilities.sounds.commands.setSelectedSound.NAME] = capability_handlers.handle_set_selected_sound + }, + [capabilities.videoStreamSettings.ID] = { + [capabilities.videoStreamSettings.commands.setStream.NAME] = capability_handlers.handle_set_stream + }, + [capabilities.cameraViewportSettings.ID] = { + [capabilities.cameraViewportSettings.commands.setDefaultViewport.NAME] = capability_handlers.handle_set_default_viewport + }, + [capabilities.localMediaStorage.ID] = { + [capabilities.localMediaStorage.commands.setLocalSnapshotRecording.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled), + [capabilities.localMediaStorage.commands.setLocalVideoRecording.NAME] = capability_handlers.set_enabled_factory(clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled) + } + }, + can_handle = require("sub_drivers.camera.can_handle") +} + +return camera_handler diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f66773d77f..c350a7adaf 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -36,7 +36,10 @@ SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, + CAMERA = 0x0142, + CHIME = 0x0146, DIMMABLE_PLUG_IN_UNIT = 0x010B, + DOORBELL = 0x0143, ELECTRICAL_SENSOR = 0x0510, GENERIC_SWITCH = 0x000F, MOUNTED_ON_OFF_CONTROL = 0x010F, @@ -177,24 +180,38 @@ SwitchFields.OPTIONS_OVERRIDE = 0x01 SwitchFields.supported_capabilities = { + capabilities.audioMute, + capabilities.audioRecording, + capabilities.audioVolume, capabilities.battery, capabilities.batteryLevel, capabilities.button, + capabilities.cameraPrivacyMode, + capabilities.cameraViewportSettings, capabilities.colorControl, capabilities.colorTemperature, capabilities.energyMeter, capabilities.fanMode, capabilities.fanSpeedPercent, + capabilities.hdr, capabilities.illuminanceMeasurement, + capabilities.imageControl, capabilities.level, + capabilities.localMediaStorage, + capabilities.mechanicalPanTiltZoom, capabilities.motionSensor, + capabilities.nightVision, capabilities.powerMeter, capabilities.powerConsumptionReport, capabilities.relativeHumidityMeasurement, + capabilities.sounds, capabilities.switch, capabilities.switchLevel, capabilities.temperatureMeasurement, capabilities.valve, + capabilities.videoStreamSettings, + capabilities.webrtc, + capabilities.zoneManagement } SwitchFields.device_type_attribute_map = { diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 0f7ac435cb..46b82678a4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -1,11 +1,13 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local MatterDriver = require "st.matter.driver" local fields = require "switch_utils.fields" local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" local log = require "log" +local version = require "version" local utils = {} @@ -166,16 +168,74 @@ function utils.component_to_endpoint(device, component) return utils.find_default_endpoint(device) end -function utils.endpoint_to_component(device, ep) - local map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {} - for component, endpoint in pairs(map) do - if endpoint == ep then +--- An extension of the library function endpoint_to_component, to support a mapping scheme +--- that includes cluster and attribute id's so that we can use multiple components for a +--- single endpoint. +--- +--- @param device any a Matter device object +--- @param opts number|table either is an ep_id or a table { endpoint_id, capability_id } +--- @return string component +function utils.endpoint_to_component(device, opts) + local ep_info = {} + if type(opts) == "number" then + ep_info.endpoint_id = opts + elseif type(opts) == "table" then + if opts.endpoint_info then + ep_info = opts.endpoint_info + else + ep_info = { + endpoint_id = opts.endpoint_id, + cluster_id = opts.cluster_id, + attribute_id = opts.attribute_id + } + end + end + for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do + if type(map_info) == "number" and map_info == ep_info.endpoint_id then return component + elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id then + if (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id + and utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id))) + and (not opts.capability_id or utils.tbl_contains(map_info.capability_ids, opts.capability_id)) then + return component + end end end return "main" end +--- An extension of the library function emit_event_for_endpoint, to support devices with +--- multiple components defined for the same endpoint, since they can't be easily +--- differentiated based on a simple endpoint id to component mapping, but we can extend +--- this mapping to include the cluster and attribute id's so that we know which component +--- to route events to. +--- +--- @param device any a Matter device object +--- @param ep_info number|table endpoint_id or ib (includes endpoint_id, cluster_id, attribute_id) +--- @param event any a capability event object +function utils.emit_event_for_endpoint(device, ep_info, event) + if type(ep_info) == "number" then + ep_info = { endpoint_id = ep_info } + elseif type(ep_info) == "table" then + ep_info = { + endpoint_id = ep_info.endpoint_id, + cluster_id = ep_info.cluster_id, + attribute_id = ep_info.attribute_id + } + end + if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then + local child = utils.find_child(device, ep_info.endpoint_id) + if child ~= nil then + child:emit_event(event) + return + end + end + local opts = { endpoint_info = ep_info, capability_id = event.capability.ID } + local comp_id = utils.endpoint_to_component(device, opts) + local comp = device.profile.components[comp_id] + device:emit_component_event(comp, event) +end + function utils.find_child(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) end @@ -268,4 +328,14 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import end end +function utils.lazy_load_if_possible(sub_driver_name) + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end + return utils diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua new file mode 100644 index 0000000000..b9804a2a76 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -0,0 +1,1814 @@ +-- Copyright © 2025 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" + +test.disable_startup_messages() + +local CAMERA_EP, FLOODLIGHT_EP, CHIME_EP, DOORBELL_EP = 1, 2, 3, 4 + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + 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 | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.AUDIO | + clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE | + clusters.CameraAvStreamManagement.types.Feature.PRIVACY | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.IMAGE_CONTROL | + clusters.CameraAvStreamManagement.types.Feature.SPEAKER | + clusters.CameraAvStreamManagement.types.Feature.HIGH_DYNAMIC_RANGE | + clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION | + clusters.CameraAvStreamManagement.types.Feature.WATERMARK | + clusters.CameraAvStreamManagement.types.Feature.ON_SCREEN_DISPLAY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, + feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportProvider.ID, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.WebRTCTransportRequestor.ID, + cluster_type = "CLIENT" + }, + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + }, + { + endpoint_id = FLOODLIGHT_EP, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30} + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = CHIME_EP, + clusters = { + { + cluster_id = clusters.Chime.ID, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x0146, device_type_revision = 1} -- Chime + } + }, + { + endpoint_id = DOORBELL_EP, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER", + } + }, + device_types = { + {device_type_id = 0x0143, device_type_revision = 1} -- Doorbell + } + } + } +}) + +local subscribe_request +local subscribed_attributes = { + clusters.CameraAvStreamManagement.attributes.AttributeList, +} + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + local floodlight_child_device_data = { + profile = t_utils.get_profile_definition("light-color-level.yml"), + device_network_id = string.format("%s:%d", mock_device.id, FLOODLIGHT_EP), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) + } + test.mock_device.add_test_device(test.mock_device.build_test_child_device(floodlight_child_device_data)) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Floodlight 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", FLOODLIGHT_EP) + }) + subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, attr in ipairs(subscribed_attributes) do + if i > 1 then subscribe_request:merge(attr:subscribe(mock_device)) end + end + 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" }) +end + +test.set_test_init_function(test_init) + +local function update_device_profile() + test.socket.matter:__set_channel_ordering("relaxed") + local uint32 = require "st.matter.data_types.Uint32" + local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "localMediaStorage", + "audioRecording", + "cameraPrivacyMode", + "imageControl", + "hdr", + "nightVision", + "mechanicalPanTiltZoom", + "videoStreamSettings", + "zoneManagement", + "webrtc", + "motionSensor", + "sounds", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + }, + { + "speaker", + { + "audioMute", + "audioVolume" + } + }, + { + "microphone", + { + "audioMute", + "audioVolume" + } + }, + { + "doorbell", + { + "button" + } + } + }, + profile = "camera" + } + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + mock_device:expect_metadata_update(expected_metadata) + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.supportedFeatures( + {audio="sendrecv", bundle=true, order="audio/video", supportTrickleICE=true, turnSource="player", video="recvonly"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "zoom", "zoomRange", "presets", "maxPresets"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation", "perZoneSensitivity"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.localMediaStorage.supportedAttributes( + {"localVideoRecording"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.audioRecording.audioRecording("enabled")) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports", "watermark", "onScreenDisplay"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes( + {"softRecordingPrivacyMode", "softLivestreamPrivacyMode"} + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedCommands( + {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} + )) + ) + local additional_subscribed_attributes = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation, + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum, + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical, + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn, + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport, + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.AttributeList, + 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, + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.OccupancySensing.attributes.Occupancy, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete + } + for _, attr in ipairs(additional_subscribed_attributes) do + subscribe_request:merge(attr:subscribe(mock_device)) + end + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +-- Matter Handler UTs + +test.register_coroutine_test( + "Reports mapping to EnabledState capability data type should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_capability_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.HDRModeEnabled, capability = capabilities.hdr.hdr}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipHorizontal, capability = capabilities.imageControl.imageFlipHorizontal}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.ImageFlipVertical, capability = capabilities.imageControl.imageFlipVertical}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftRecordingPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softRecordingPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.SoftLivestreamPrivacyModeEnabled, capability = capabilities.cameraPrivacyMode.softLivestreamPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.HardPrivacyModeOn, capability = capabilities.cameraPrivacyMode.hardPrivacyMode}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalSnapshotRecordingEnabled, capability = capabilities.localMediaStorage.localSnapshotRecording}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.LocalVideoRecordingEnabled, capability = capabilities.localMediaStorage.localVideoRecording} + } + for _, v in ipairs(cluster_to_capability_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("enabled")) + ) + if v.capability == capabilities.imageControl.imageFlipHorizontal then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal"})) + ) + elseif v.capability == capabilities.imageControl.imageFlipVertical then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageFlipHorizontal", "imageFlipVertical"})) + ) + elseif v.capability == capabilities.cameraPrivacyMode.hardPrivacyMode then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraPrivacyMode.supportedAttributes({"softRecordingPrivacyMode", "softLivestreamPrivacyMode", "hardPrivacyMode"})) + ) + end + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("disabled")) + ) + end + end +) + +test.register_coroutine_test( + "Night Vision reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_capability_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVision, capability = capabilities.nightVision.nightVision}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.NightVisionIllum, capability = capabilities.nightVision.illumination} + } + for _, v in ipairs(cluster_to_capability_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("off")) + ) + if v.capability == capabilities.nightVision.illumination then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.nightVision.supportedAttributes({"illumination"})) + ) + end + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("on")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", v.capability("auto")) + ) + end + end +) + +test.register_coroutine_test( + "Image Rotation reports should generate appropriate events", + function() + local utils = require "st.utils" + update_device_profile() + test.wait_for_events() + local first_value = true + for angle = 0, 400, 50 do + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.ImageRotation:build_test_report_data(mock_device, CAMERA_EP, angle) + }) + local clamped_angle = utils.clamp_value(angle, 0, 359) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.imageRotation(clamped_angle)) + ) + if first_value then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.imageControl.supportedAttributes({"imageRotation"})) + ) + first_value = false + end + end + end +) + +test.register_coroutine_test( + "Two Way Talk Support reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.HALF_DUPLEX + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(true)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkbackDuplex("halfDuplex")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.FULL_DUPLEX + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(true)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkbackDuplex("fullDuplex")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.TwoWayTalkSupport:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TwoWayTalkSupportTypeEnum.NOT_SUPPORTED + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.webrtc.talkback(false)) + ) + end +) + +test.register_coroutine_test( + "Muted reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local cluster_to_component_map = { + {cluster = clusters.CameraAvStreamManagement.server.attributes.SpeakerMuted, component = "speaker"}, + {cluster = clusters.CameraAvStreamManagement.server.attributes.MicrophoneMuted, component = "microphone"} + } + for _, v in ipairs(cluster_to_component_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioMute.mute("muted")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioMute.mute("unmuted")) + ) + end + end +) + +test.register_coroutine_test( + "Volume Level reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + local max_vol = 200 + local min_vol = 0 + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.wait_for_events() + local cluster_to_component_map = { + { cluster = clusters.CameraAvStreamManagement.server.attributes.SpeakerVolumeLevel, component = "speaker"}, + { cluster = clusters.CameraAvStreamManagement.server.attributes.MicrophoneVolumeLevel, component = "microphone"} + } + for _, v in ipairs(cluster_to_component_map) do + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, 130) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioVolume.volume(65)) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + v.cluster:build_test_report_data(mock_device, CAMERA_EP, 64) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(v.component, capabilities.audioVolume.volume(32)) + ) + end + end +) + +test.register_coroutine_test( + "Status Light Enabled reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.on()) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:build_test_report_data(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.switch.switch.off()) + ) + end +) + +test.register_coroutine_test( + "Status Light Brightness reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.LOW) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedModes( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.supportedArguments( + {"low", "medium", "high", "auto"}, {visibility = {displayed = false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("low")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.MEDIUM) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("medium")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.HIGH) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("high")) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:build_test_report_data( + mock_device, CAMERA_EP, clusters.Global.types.ThreeLevelAutoEnum.AUTO) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("statusLed", capabilities.mode.mode("auto")) + ) + end +) + +local function receive_rate_distortion_trade_off_points() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.CameraAvStreamManagement.types.RateDistortionTradeOffPointsStruct({ + codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.H264, + resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 1920, + height = 1080 + }), + min_bit_rate = 5000000 + }), + clusters.CameraAvStreamManagement.types.RateDistortionTradeOffPointsStruct({ + codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.HEVC, + resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 3840, + height = 2160 + }), + min_bit_rate = 20000000 + }) + } + ) + }) +end + +local function receive_max_encoded_pixel_rate() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate:build_test_report_data( + mock_device, CAMERA_EP, 124416000) -- 1080p @ 60 fps or 4K @ 15 fps + }) +end + +local function receive_video_sensor_params() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.VideoSensorParamsStruct({ + sensor_width = 7360, + sensor_height = 4912, + max_fps = 60, + max_hdrfps = 30 + }) + ) + }) +end + +local function emit_video_sensor_parameters() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraViewportSettings.videoSensorParameters({ + width = 7360, + height = 4912, + maxFPS = 60 + })) + ) +end + +local function emit_supported_resolutions() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.supportedResolutions({ + { + width = 1920, + height = 1080, + fps = 60 + }, + { + width = 3840, + height = 2160, + fps = 15 + } + })) + ) +end + +-- Test receiving RateDistortionTradeOffPoints, MaxEncodedPixelRate, and VideoSensorParams in various orders +-- to ensure that cameraViewportSettings and videoStreamSettings capabilities are updated as expected. Note that +-- cameraViewportSettings.videoSensorParameters is set in the VideoSensorParams handler and +-- videoStreamSettings.supportedResolutions is emitted after all three attributes are received. + +test.register_coroutine_test( + "Rate Distortion Trade Off Points, MaxEncodedPixelRate, VideoSensorParams reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_rate_distortion_trade_off_points() + receive_max_encoded_pixel_rate() + receive_video_sensor_params() + emit_video_sensor_parameters() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "Rate Distortion Trade Off Points, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_rate_distortion_trade_off_points() + receive_video_sensor_params() + emit_video_sensor_parameters() + receive_max_encoded_pixel_rate() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "MaxEncodedPixelRate, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + receive_max_encoded_pixel_rate() + receive_video_sensor_params() + emit_video_sensor_parameters() + receive_rate_distortion_trade_off_points() + emit_supported_resolutions() + end +) + +test.register_coroutine_test( + "PTZ Position reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax:build_test_report_data(mock_device, CAMERA_EP, 150) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin:build_test_report_data(mock_device, CAMERA_EP, -150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.panRange({value = {minimum = -150, maximum = 150}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax:build_test_report_data(mock_device, CAMERA_EP, 80) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin:build_test_report_data(mock_device, CAMERA_EP, -80) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tiltRange({value = {minimum = -80, maximum = 80}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax:build_test_report_data(mock_device, CAMERA_EP, 70) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoomRange({value = {minimum = 1, maximum = 70}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 10, tilt = 20, zoom = 30}) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(10)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(20)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(30)) + ) + end +) + +test.register_coroutine_test( + "PTZ Presets reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets:build_test_report_data( + mock_device, CAMERA_EP, {{preset_id = 1, name = "Preset 1", settings = {pan = 10, tilt = 20, zoom = 30}}, + {preset_id = 2, name = "Preset 2", settings = {pan = -55, tilt = 80, zoom = 60}}} + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.presets({ + { id = 1, label = "Preset 1", pan = 10, tilt = 20, zoom = 30}, + { id = 2, label = "Preset 2", pan = -55, tilt = 80, zoom = 60} + })) + ) + end +) + +test.register_coroutine_test( + "Max Presets reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets:build_test_report_data(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.maxPresets(10)) + ) + end +) + +test.register_coroutine_test( + "Max Zones reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.MaxZones:build_test_report_data(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.maxZones(10)) + ) + end +) + +test.register_coroutine_test( + "Zones reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Zones:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneInformationStruct({ + zone_id = 1, + zone_type = clusters.ZoneManagement.types.ZoneTypeEnum.TWODCART_ZONE, + zone_source = clusters.ZoneManagement.types.ZoneSourceEnum.MFG, + two_d_cartesian_zone = clusters.ZoneManagement.types.TwoDCartesianZoneStruct({ + name = "Zone 1", + use = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 0, y = 0 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1920, y = 1080 }) + }, + color = "#FFFFFF" + }) + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.zones({ + { + id = 1, + name = "Zone 1", + type = "2DCartesian", + polygonVertices = { + {vertex = {x = 0, y = 0}}, + {vertex = {x = 1920, y = 1080}} + }, + source = "manufacturer", + use = "motion", + color = "#FFFFFF" + } + })) + ) + end +) + +test.register_coroutine_test( + "Triggers reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, + initial_duration = 8, + augmentation_duration = 4, + max_duration = 20, + blind_duration = 3, + sensitivity = 4 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({ + { + zoneId = 1, + initialDuration = 8, + augmentationDuration = 4, + maxDuration = 20, + blindDuration = 3, + sensitivity = 4 + } + })) + ) + end +) + +test.register_coroutine_test( + "Sensitivity reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.SensitivityMax:build_test_report_data(mock_device, CAMERA_EP, 7) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.sensitivityRange({ minimum = 1, maximum = 7}, + {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Sensitivity:build_test_report_data(mock_device, CAMERA_EP, 5) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.sensitivity(5, {visibility = {displayed = false}})) + ) + end +) + +test.register_coroutine_test( + "Chime reports should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Chime.attributes.InstalledChimeSounds:build_test_report_data(mock_device, CAMERA_EP, { + clusters.Chime.types.ChimeSoundStruct({chime_id = 1, name = "Sound 1"}), + clusters.Chime.types.ChimeSoundStruct({chime_id = 2, name = "Sound 2"}) + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.sounds.supportedSounds({ + {id = 1, label = "Sound 1"}, + {id = 2, label = "Sound 2"}, + }, {visibility = {displayed = false}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Chime.attributes.SelectedChime:build_test_report_data(mock_device, CAMERA_EP, 2) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.sounds.selectedSound(2))) + end +) + +-- Event Handler UTs + +test.register_coroutine_test( + "Zone events should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneTriggered:build_test_event_report(mock_device, CAMERA_EP, { + zone = 3, + reason = clusters.ZoneManagement.types.ZoneEventTriggeredReasonEnum.MOTION + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 2}, {zoneId = 3}})) + ) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.events.ZoneStopped:build_test_event_report(mock_device, CAMERA_EP, { + zone = 2, + reason = clusters.ZoneManagement.types.ZoneEventStoppedReasonEnum.ACTION_STOPPED + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggeredZones({{zoneId = 3}})) + ) + end +) + +test.register_coroutine_test( + "Button events should generate appropriate events", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.server.events.InitialPress:build_test_event_report(mock_device, DOORBELL_EP, {new_position = 1}) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.server.events.MultiPressComplete:build_test_event_report(mock_device, DOORBELL_EP, { + new_position = 1, + total_number_of_presses_counted = 2, + previous_position = 0 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("doorbell", capabilities.button.button.double({state_change = true})) + ) + end +) + +-- Capability Handler UTs + +test.register_coroutine_test( + "Set night vision commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local command_to_attribute_map = { + ["setNightVision"] = clusters.CameraAvStreamManagement.attributes.NightVision, + ["setIllumination"] = clusters.CameraAvStreamManagement.attributes.NightVisionIllum + } + for cmd, attr in pairs(command_to_attribute_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "off" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.OFF) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "on" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.ON) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "nightVision", component = "main", command = cmd, args = { "auto" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, attr:write(mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.TriStateAutoEnum.AUTO) + }) + end + end +) + +test.register_coroutine_test( + "Set enabled commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local command_to_attribute_map = { + ["setHdr"] = { capability = "hdr", attr = clusters.CameraAvStreamManagement.attributes.HDRModeEnabled}, + ["setImageFlipHorizontal"] = { capability = "imageControl", attr = clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal}, + ["setImageFlipVertical"] = { capability = "imageControl", attr = clusters.CameraAvStreamManagement.attributes.ImageFlipVertical}, + ["setSoftLivestreamPrivacyMode"] = { capability = "cameraPrivacyMode", attr = clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled}, + ["setSoftRecordingPrivacyMode"] = { capability = "cameraPrivacyMode", attr = clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled}, + ["setLocalSnapshotRecording"] = { capability = "localMediaStorage", attr = clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled}, + ["setLocalVideoRecording"] = { capability = "localMediaStorage", attr = clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled} + } + for i, v in pairs(command_to_attribute_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = v.capability, component = "main", command = i, args = { "enabled" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, v.attr:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = v.capability, component = "main", command = i, args = { "disabled" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, v.attr:write(mock_device, CAMERA_EP, false) + }) + end + end +) + +test.register_coroutine_test( + "Set image rotation command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "imageControl", component = "main", command = "setImageRotation", args = { 10 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.ImageRotation:write(mock_device, CAMERA_EP, 10) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "imageControl", component = "main", command = "setImageRotation", args = { 257 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.ImageRotation:write(mock_device, CAMERA_EP, 257) + }) + end +) + +test.register_coroutine_test( + "Set mute commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "setMute", args = { "muted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "setMute", args = { "unmuted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "mute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "speaker", command = "unmute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "setMute", args = { "muted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "setMute", args = { "unmuted" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, false) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "mute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioMute", component = "microphone", command = "unmute", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneMuted:write(mock_device, CAMERA_EP, false) + }) + end +) + +test.register_coroutine_test( + "Set Volume command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local max_vol = 200 + local min_vol = 5 + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.SpeakerMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMaxLevel:build_test_report_data(mock_device, CAMERA_EP, max_vol) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.server.attributes.MicrophoneMinLevel:build_test_report_data(mock_device, CAMERA_EP, min_vol) + }) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "setVolume", args = { 0 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 5) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "setVolume", args = { 35 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 73) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "setVolume", args = { 77 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 155) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "setVolume", args = { 100 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 200) + }) + + ---- test volumeUp command + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 103) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("speaker", capabilities.audioVolume.volume(50)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "speaker", command = "volumeUp", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:write(mock_device, CAMERA_EP, 104) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 104) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("speaker", capabilities.audioVolume.volume(51)) + ) + + -- test volumeDown command + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("microphone", capabilities.audioVolume.volume(100)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "audioVolume", component = "microphone", command = "volumeDown", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:write(mock_device, CAMERA_EP, 198) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel:build_test_report_data(mock_device, CAMERA_EP, 198) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("microphone", capabilities.audioVolume.volume(99)) + ) + end +) + +test.register_coroutine_test( + "Set Mode command should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local mode_to_enum_map = { + ["low"] = clusters.Global.types.ThreeLevelAutoEnum.LOW, + ["medium"] = clusters.Global.types.ThreeLevelAutoEnum.MEDIUM, + ["high"] = clusters.Global.types.ThreeLevelAutoEnum.HIGH, + ["auto"] = clusters.Global.types.ThreeLevelAutoEnum.AUTO + } + for i, v in pairs(mode_to_enum_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "speaker", command = "setMode", args = { i } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness:write(mock_device, CAMERA_EP, v) + }) + end + end +) + +test.register_coroutine_test( + "Set Status LED commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "on", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, true) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "statusLed", command = "off", args = { } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled:write(mock_device, CAMERA_EP, false) + }) + end +) + +test.register_coroutine_test( + "Set Relative PTZ commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { 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) + }) + 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) + }) + 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) + }) + end +) + +test.register_coroutine_test( + "Set PTZ commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "setPanTiltZoom", args = { 10, 20, 30 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 10, 20, 30) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 10, tilt = 20, zoom = 30}) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(10)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(20)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(30)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { 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) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = 20, zoom = 30}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(50)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { 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) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = -44, zoom = 30}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(-44)) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { 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) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device, CAMERA_EP, {pan = 50, tilt = -44, zoom = 5}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.mechanicalPanTiltZoom.zoom(5)) + ) + end +) + +test.register_coroutine_test( + "Preset commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "savePreset", args = { 1, "Preset 1" } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSavePreset(mock_device, CAMERA_EP, 1, "Preset 1") + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "removePreset", args = { 1 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRemovePreset(mock_device, CAMERA_EP, 1) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mechanicalPanTiltZoom", component = "main", command = "moveToPreset", args = { 2 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZMoveToPreset(mock_device, CAMERA_EP, 2) + }) + end +) + +test.register_coroutine_test( + "Sound commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "sounds", component = "main", command = "setSelectedSound", args = { 1 } }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.Chime.attributes.SelectedChime:write(mock_device, CAMERA_EP, 1) + }) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "sounds", component = "main", command = "playSound", args = {} }, + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.Chime.server.commands.PlayChimeSound(mock_device, CAMERA_EP) + }) + end +) + +test.register_coroutine_test( + "Zone Management zone commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + local use_map = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "blue" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + color = "blue" + } + ) + ) + }) + end + local zone_id = 1 + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "updateZone", args = { + zone_id, "updated " .. i .. " zone", {{value = {x = 50, y = 50}}, {value = {x = 1000, y = 1000}} }, i, "red" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone(mock_device, CAMERA_EP, + zone_id, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = "updated " .. i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 50, y = 50 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1000, y = 1000 }) + }, + color = "red" + } + ) + ) + }) + zone_id = zone_id + 1 + end + for i = 1, 3 do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { i } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, i) + }) + end + end +) + +test.register_coroutine_test( + "Zone Management zone commands should send the appropriate commands - missing optional color argument", + function() + update_device_profile() + test.wait_for_events() + local use_map = { + ["motion"] = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + ["focus"] = clusters.ZoneManagement.types.ZoneUseEnum.FOCUS, + ["privacy"] = clusters.ZoneManagement.types.ZoneUseEnum.PRIVACY + } + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + } + ) + ) + }) + end + local zone_id = 1 + for i, v in pairs(use_map) do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "updateZone", args = { + zone_id, "updated " .. i .. " zone", {{value = {x = 50, y = 50}}, {value = {x = 1000, y = 1000}} }, i, "red" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.UpdateTwoDCartesianZone(mock_device, CAMERA_EP, + zone_id, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct( + { + name = "updated " .. i .. " zone", + use = v, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 50, y = 50 }), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({ x = 1000, y = 1000 }) + }, + color = "red" + } + ) + ) + }) + zone_id = zone_id + 1 + end + for i = 1, 3 do + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { i } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, i) + }) + end + end +) + +test.register_coroutine_test( + "Zone Management trigger commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + + -- Create the trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3, + sensitivity = 5 + }) + }) + + -- The device reports the Triggers attribute with the newly created trigger and the capability is updated + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, initial_duration = 10, augmentation_duration = 3, max_duration = 15, blind_duration = 3, sensitivity = 5 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({{ + zoneId = 1, initialDuration = 10, augmentationDuration = 3, maxDuration = 15, blindDuration = 3, sensitivity = 5 + }})) + ) + test.wait_for_events() + + -- Update trigger, note that some arguments are optional. In this case, + -- blindDuration is not specified in the capability command. + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 8, 7, 25, 3, 1 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 8, + augmentation_duration = 7, + max_duration = 25, + blind_duration = 3, + sensitivity = 1 + }) + }) + + -- Remove the trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeTrigger", args = { 1 } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveTrigger(mock_device, CAMERA_EP, 1) + }) + end +) + +test.register_coroutine_test( + "Stream management commands should send the appropriate commands", + function() + update_device_profile() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "videoStreamSettings", component = "main", command = "setStream", args = { + 3, + "liveStream", + "Stream 3", + { width = 1920, height = 1080, fps = 30 }, + { upperLeftVertex = {x = 0, y = 0}, lowerRightVertex = {x = 1920, y = 1080} }, + "enabled", + "disabled" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(mock_device, CAMERA_EP, + 3, true, false + ) + }) + end +) + +test.register_coroutine_test( + "Stream management setStream command should modify an existing stream", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.CameraAvStreamManagement.types.VideoStreamStruct({ + video_stream_id = 1, + stream_usage = clusters.Global.types.StreamUsageEnum.LIVE_VIEW, + video_codec = clusters.CameraAvStreamManagement.types.VideoCodecEnum.H264, + min_frame_rate = 30, + max_frame_rate = 60, + min_resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({width = 640, height = 360}), + max_resolution = clusters.CameraAvStreamManagement.types.VideoResolutionStruct({width = 640, height = 360}), + min_bit_rate = 10000, + max_bit_rate = 10000, + key_frame_interval = 4000, + watermark_enabled = true, + osd_enabled = false, + reference_count = 0 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.videoStreamSettings.videoStreams({ + { + streamId = 1, + data = { + label = "Stream 1", + type = "liveStream", + resolution = { + width = 640, + height = 360, + fps = 30 + }, + watermark = "enabled", + onScreenDisplay = "disabled" + } + } + })) + ) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "videoStreamSettings", component = "main", command = "setStream", args = { + 1, + "liveStream", + "Stream 1", + { width = 640, height = 360, fps = 30 }, + { upperLeftVertex = {x = 0, y = 0}, lowerRightVertex = {x = 640, y = 360} }, + "disabled", + "enabled" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.CameraAvStreamManagement.server.commands.VideoStreamModify(mock_device, CAMERA_EP, + 1, false, true + ) + }) + end +) + +-- run the tests +test.run_registered_tests() From a4e2a024608bfa5e71f4acdd5e628992705f0575 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:38:11 -0600 Subject: [PATCH 264/449] Matter Switch: Write EXECUTE_IF_OFF bit to Control Cluster Options attribute (#2535) --- .../SmartThings/matter-switch/src/init.lua | 1 + .../src/switch_utils/device_configuration.lua | 15 +++++++ .../matter-switch/src/switch_utils/utils.lua | 16 ++++++++ .../src/test/test_electrical_sensor.lua | 1 + .../src/test/test_matter_light_fan.lua | 2 + .../test_matter_multi_button_switch_mcd.lua | 3 ++ .../src/test/test_matter_switch.lua | 21 +++++++++- .../test/test_matter_switch_device_types.lua | 39 +++++++++++++++++- .../test_multi_switch_parent_child_lights.lua | 41 +++++++++++-------- 9 files changed, 119 insertions(+), 20 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 403a14a991..793160e285 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -40,6 +40,7 @@ end function SwitchLifecycleHandlers.do_configure(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) end 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 626080b147..adc0da500e 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -72,6 +72,21 @@ function SwitchDeviceConfiguration.create_child_devices(driver, device, server_o device:set_find_child(switch_utils.find_child) end +-- Per the spec, these attributes are "meant to be changed only during commissioning." +function SwitchDeviceConfiguration.set_device_control_options(device) + for _, ep_info in ipairs(device.endpoints) do + -- before the Matter 1.3 lua libs update (HUB FW 54), OptionsBitmap was defined as LevelControlOptions + if switch_utils.ep_supports_cluster(ep_info, clusters.LevelControl.ID) then + device:send(clusters.LevelControl.attributes.Options:write(device, ep_info.endpoint_id, clusters.LevelControl.types.LevelControlOptions.EXECUTE_IF_OFF)) + end + -- before the Matter 1.4 lua libs update (HUB FW 56), there was no OptionsBitmap type defined + if switch_utils.ep_supports_cluster(ep_info, clusters.ColorControl.ID) then + local excute_if_off_bit = clusters.ColorControl.types.OptionsBitmap and clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF or 0x0001 + device:send(clusters.ColorControl.attributes.Options:write(device, ep_info.endpoint_id, excute_if_off_bit)) + end + end +end + function ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, num_button_eps) local profile_name = string.gsub(num_button_eps .. "-button", "1%-", "") -- remove the "1-" in a device with 1 button ep if switch_utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 46b82678a4..69652f37fb 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -247,6 +247,22 @@ function utils.get_endpoint_info(device, endpoint_id) return {} end +function utils.ep_supports_cluster(ep_info, cluster_id, opts) + opts = opts or {} + local clus_has_features = function(cluster, checked_feature) + return (cluster.feature_map & checked_feature) == checked_feature + end + for _, cluster in ipairs(ep_info.clusters) do + if ((cluster.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(cluster, opts.feature_bitmap)) + and ((opts.cluster_type == nil and cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH") + or (opts.cluster_type == cluster.cluster_type)) + or (cluster_id == nil)) then + return true + end + end +end + -- Fallback handler for responses that dont have their own handler function utils.matter_handler(driver, device, response_block) device.log.info(string.format("Fallback handler for %s", response_block)) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua index b9db2a92b2..21286fc55f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua @@ -408,6 +408,7 @@ test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, 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 d50d16b8fe..a35f5bda50 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 @@ -87,6 +87,8 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end 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 ab35eff35c..0eb10ba6f7 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 @@ -226,6 +226,9 @@ local function test_init() parent_assigned_child_key = string.format("%d", mock_device_ep5) }) expect_configure_buttons() + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) -- simulate the profile change update taking affect and the device info changing 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 ba9ae3c921..e38003ce5a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -200,7 +200,7 @@ local function test_init_color_temp() subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) end end - test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "added" }) test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) @@ -208,7 +208,16 @@ local function test_init_color_temp() test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_color_temp.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.LevelControl.attributes.Options:write(mock_device_color_temp, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_color_temp.id, + clusters.ColorControl.attributes.Options:write(mock_device_color_temp, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_color_temp:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) end local function test_init_extended_color() @@ -221,13 +230,21 @@ local function test_init_extended_color() end test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "added" }) - test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "init" }) test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_extended_color.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_extended_color.id, + clusters.LevelControl.attributes.Options:write(mock_device_extended_color, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_extended_color.id, + clusters.ColorControl.attributes.Options:write(mock_device_extended_color, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_extended_color:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.matter:__expect_send({mock_device_extended_color.id, subscribe_request}) end test.register_message_test( diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 560230cdc7..920b9a4b5f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -143,7 +143,7 @@ local mock_device_mounted_on_off_control = test.mock_device.build_test_matter_de endpoint_id = 7, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, }, device_types = { @@ -173,7 +173,7 @@ local mock_device_mounted_dimmable_load_control = test.mock_device.build_test_ma endpoint_id = 7, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0}, - {cluster_id = clusters.LevelControl.ID, cluster_type = "CLIENT", feature_map = 2}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, }, device_types = { @@ -420,6 +420,10 @@ local function test_init_parent_child_switch_types() test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_switch_types.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_switch_types, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_parent_child_switch_types:expect_metadata_update({ profile = "switch-level" }) mock_device_parent_child_switch_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -463,6 +467,10 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_dimmer.id, + clusters.LevelControl.attributes.Options:write(mock_device_dimmer, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_dimmer:expect_metadata_update({ profile = "switch-level" }) mock_device_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -494,6 +502,10 @@ local function test_init_mounted_on_off_control() test.socket.matter:__expect_send({mock_device_mounted_on_off_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_on_off_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_on_off_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_on_off_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_mounted_on_off_control:expect_metadata_update({ profile = "switch-binary" }) mock_device_mounted_on_off_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -502,6 +514,9 @@ local function test_init_mounted_dimmable_load_control() test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.LevelControl.attributes.MaxLevel, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mounted_dimmable_load_control) for i, cluster in ipairs(cluster_subscribe_list) do @@ -516,6 +531,10 @@ local function test_init_mounted_dimmable_load_control() test.socket.matter:__expect_send({mock_device_mounted_dimmable_load_control.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_mounted_dimmable_load_control.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_mounted_dimmable_load_control.id, + clusters.LevelControl.attributes.Options:write(mock_device_mounted_dimmable_load_control, 7, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ profile = "switch-level" }) mock_device_mounted_dimmable_load_control:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -557,6 +576,14 @@ local function test_init_parent_child_different_types() test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_different_types.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) + test.socket.matter:__expect_send({ + mock_device_parent_child_different_types.id, + clusters.ColorControl.attributes.Options:write(mock_device_parent_child_different_types, 10, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_parent_child_different_types:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_different_types:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -575,6 +602,10 @@ local function test_init_parent_child_unsupported_device_type() test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_unsupported_device_type.id, "doConfigure" }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ profile = "switch-binary" }) + test.socket.matter:__expect_send({ + mock_device_parent_child_unsupported_device_type.id, + clusters.LevelControl.attributes.Options:write(mock_device_parent_child_unsupported_device_type, 10, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_parent_child_unsupported_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) mock_device_parent_child_unsupported_device_type:expect_device_create({ @@ -609,6 +640,10 @@ local function test_init_light_level_motion() test.socket.matter:__expect_send({mock_device_light_level_motion.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_light_level_motion.id, "doConfigure" }) + test.socket.matter:__expect_send({ + mock_device_light_level_motion.id, + clusters.LevelControl.attributes.Options:write(mock_device_light_level_motion, 1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF) + }) mock_device_light_level_motion:expect_metadata_update({ profile = "light-level-motion" }) mock_device_light_level_motion:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 0d3cd90854..178a5bf37d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -178,6 +178,9 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -222,7 +225,9 @@ for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endp end local function test_init_parent_child_endpoints_non_sequential() - test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) + local unsup_mock_device = mock_device_parent_child_endpoints_non_sequential + + test.mock_device.add_test_device(unsup_mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -236,49 +241,53 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) for i, cluster in ipairs(cluster_subscribe_list) do if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end end - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) - test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({unsup_mock_device.id, clusters.ColorControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + unsup_mock_device:expect_metadata_update({ profile = "light-binary" }) + unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", profile = "light-color-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) }) -- switch-binary will be selected as an overridden child device profile - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", profile = "switch-binary", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) }) - mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 4", profile = "light-level", - parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) }) end From 9fa64344f94356933cd08cfa46e24965eba65e2d Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 17 Nov 2025 10:56:46 -0600 Subject: [PATCH 265/449] Lazy load camera subdriver only for LuaLibs 16 and greater (#2558) The camera subdriver is only supported on LuaLibs version >= 16, and causes issues if it's loaded on previous versions, so this commit changes `lazy_load_if_possible` to `lazy_load`, which only attempts to pull in the subdriver if the api version if 16 or greater. --- drivers/SmartThings/matter-switch/src/init.lua | 2 +- .../SmartThings/matter-switch/src/switch_utils/utils.lua | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 793160e285..ec3c5df9ba 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -297,7 +297,7 @@ local matter_driver_template = { supported_capabilities = fields.supported_capabilities, sub_drivers = { require("sub_drivers.aqara_cube"), - switch_utils.lazy_load_if_possible("sub_drivers.camera"), + switch_utils.lazy_load("sub_drivers.camera"), require("sub_drivers.eve_energy"), require("sub_drivers.third_reality_mk1") } diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 69652f37fb..718e186dcf 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -344,13 +344,9 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import end end -function utils.lazy_load_if_possible(sub_driver_name) +function utils.lazy_load(sub_driver_name) if version.api >= 16 then return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) - elseif version.api >= 9 then - return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) end end From 03e2fc15d73cfddb846bf8292e69f416ff70e40c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 11 Nov 2025 12:32:09 -0800 Subject: [PATCH 266/449] Merge pull request #2543 from SmartThingsCommunity/new_device/WWSTCERT-8812 WWSTCERT-8812 Heiman Smart Humidity&Temperature Sensor --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index af05f7bafe..c1e2f2dc04 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -74,6 +74,11 @@ matterManufacturer: vendorId: 0x120B productId: 0x1008 deviceProfileName: leak-battery + - id: "4619/4192" + deviceLabel: Smart Humidity&Temperature Sensor + vendorId: 0x120B + productId: 0x1060 + deviceProfileName: temperature-humidity-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor From 04849b4cf8db7df12280e89548a8ab4ced687282 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 14 Nov 2025 10:38:00 -0800 Subject: [PATCH 267/449] Merge pull request #2553 from seojune79/Aqara-WirelessRemote-HeldEvt [Aqara] Fixed an issue where an unintended event was triggered (or generated) when the button was long-pressed --- .../zigbee-button/src/aqara/init.lua | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index b405b506cd..f3defc54af 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -57,17 +57,19 @@ local configuration = { } local function present_value_attr_handler(driver, device, value, zb_rx) - local end_point = zb_rx.address_header.src_endpoint.value - local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 - local evt = capabilities.button.button.held({ state_change = true }) - if value.value == 1 then - evt = capabilities.button.button.pushed({ state_change = true }) - elseif value.value == 2 then - evt = capabilities.button.button.double({ state_change = true }) - end - device:emit_event(evt) - if btn_evt_cnt > 1 then - device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) + if value.value < 0xFF then + local end_point = zb_rx.address_header.src_endpoint.value + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local evt = capabilities.button.button.held({ state_change = true }) + if value.value == 1 then + evt = capabilities.button.button.pushed({ state_change = true }) + elseif value.value == 2 then + evt = capabilities.button.button.double({ state_change = true }) + end + device:emit_event(evt) + if btn_evt_cnt > 1 then + device:emit_component_event(device.profile.components[COMP_LIST[end_point]], evt) + end end end local function battery_level_handler(driver, device, value, zb_rx) From ea8cbe05db0f03099fb29b6a4d9fb1285b76ee0b Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:03 -0600 Subject: [PATCH 268/449] zigbee-power-meter:[automated] lazy loading changes --- .../src/ezex/can_handle.lua | 15 +++++++++ .../src/ezex/fingerprints.lua | 8 +++++ .../zigbee-power-meter/src/ezex/init.lua | 30 +++-------------- .../src/frient/can_handle.lua | 15 +++++++++ .../src/frient/fingerprints.lua | 9 ++++++ .../zigbee-power-meter/src/frient/init.lua | 31 +++--------------- .../zigbee-power-meter/src/init.lua | 22 +++---------- .../src/lazy_load_subdriver.lua | 15 +++++++++ .../src/shinasystems/can_handle.lua | 15 +++++++++ .../src/shinasystems/fingerprints.lua | 10 ++++++ .../src/shinasystems/init.lua | 32 +++---------------- .../zigbee-power-meter/src/sub_drivers.lua | 10 ++++++ 12 files changed, 113 insertions(+), 99 deletions(-) create mode 100644 drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua new file mode 100644 index 0000000000..91569fd4b4 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ezex_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("ezex.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("ezex") + end + end + + return false +end + +return is_ezex_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua new file mode 100644 index 0000000000..1495f82c56 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "E240-KR080Z0-HA" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua index 9e4c6d6eea..15ca345f31 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/ezex/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -20,19 +10,7 @@ local ElectricalMeasurement = clusters.ElectricalMeasurement local energy_meter_defaults = require "st.zigbee.defaults.energyMeter_defaults" local configurations = require "configurations" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "E240-KR080Z0-HA" } -} -local is_ezex_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - - return false -end local instantaneous_demand_configuration = { cluster = SimpleMetering.ID, @@ -88,7 +66,7 @@ local ezex_power_meter_handler = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_ezex_power_meter + can_handle = require("ezex.can_handle"), } return ezex_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua new file mode 100644 index 0000000000..3cf7077ddd --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_frient_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("frient") + end + end + + return false +end + +return is_frient_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua new file mode 100644 index 0000000000..5bc09f600d --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "ZHEMI101" }, + { model = "EMIZB-132" }, +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua index 9d3f6f83b0..5933faf5cb 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/frient/init.lua @@ -1,34 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local constants = require "st.zigbee.constants" local configurations = require "configurations" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "ZHEMI101" }, - { model = "EMIZB-132" }, -} - -local is_frient_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configure = function(self, device) device:refresh() @@ -46,7 +23,7 @@ local frient_power_meter_handler = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_frient_power_meter + can_handle = require("frient.can_handle"), } return frient_power_meter_handler \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/src/init.lua b/drivers/SmartThings/zigbee-power-meter/src/init.lua index ae98baca8b..f15fae7905 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -63,11 +53,7 @@ local zigbee_power_meter_driver_template = { } }, current_config_version = 1, - sub_drivers = { - require("ezex"), - require("frient"), - require("shinasystems"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, diff --git a/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua new file mode 100644 index 0000000000..94808a8486 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_shinasystems_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("shinasystems") + end + end + + return false +end + +return is_shinasystems_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua new file mode 100644 index 0000000000..5364835514 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { model = "PMM-300Z1" }, + { model = "PMM-300Z2" }, + { model = "PMM-300Z3" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua index 64713547a8..54866e2f82 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/shinasystems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -19,11 +9,6 @@ local SimpleMetering = clusters.SimpleMetering local ElectricalMeasurement = clusters.ElectricalMeasurement local configurations = require "configurations" -local ZIGBEE_POWER_METER_FINGERPRINTS = { - { model = "PMM-300Z1" }, - { model = "PMM-300Z2" }, - { model = "PMM-300Z3" } -} local POWERMETER_CONFIGURATION_V2 = { { @@ -52,15 +37,6 @@ local POWERMETER_CONFIGURATION_V2 = { } } -local is_shinasystems_power_meter = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_POWER_METER_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - - return false -end local function energy_meter_handler(driver, device, value, zb_rx) local multiplier = device:get_field(constants.SIMPLE_METERING_MULTIPLIER_KEY) or 1 @@ -128,7 +104,7 @@ local shinasystems_power_meter_handler = { init = configurations.power_reconfig_wrapper(device_init), doConfigure = do_configure, }, - can_handle = is_shinasystems_power_meter + can_handle = require("shinasystems.can_handle"), } return shinasystems_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua new file mode 100644 index 0000000000..58ff064c9f --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("ezex"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("shinasystems"), +} +return sub_drivers From c178696137b62cb53b75c6ee52b2e1cc7becdfdb Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Wed, 19 Nov 2025 04:15:09 +0900 Subject: [PATCH 269/449] WWSTCERT-8722/8725 [Aqara] add Aqara Wireless Remote Switch H1(Single/Double) (#2511) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add Aqara Wireless Remote Switch H1(Single/Double) * remove the commented‑out code --- .../zigbee-button/fingerprints.yml | 10 ++ .../profiles/aqara-double-buttons-mode.yml | 35 +++++ .../profiles/aqara-single-button-mode.yml | 17 +++ .../zigbee-button/src/aqara/init.lua | 57 +++++++- .../src/test/test_aqara_button.lua | 132 ++++++++++++------ 5 files changed, 202 insertions(+), 49 deletions(-) create mode 100644 drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml create mode 100644 drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 61285448f7..c70cae5eac 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -24,6 +24,16 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.remote.b286acn03 deviceProfileName: aqara-double-buttons + - id: "LUMI/lumi.remote.b18ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Single Rocker) + manufacturer: LUMI + model: lumi.remote.b18ac1 + deviceProfileName: aqara-single-button-mode + - id: "LUMI/lumi.remote.b28ac1" + deviceLabel: Aqara Wireless Remote Switch H1 (Double Rocker) + manufacturer: LUMI + model: lumi.remote.b28ac1 + deviceProfileName: aqara-double-buttons-mode - id: "HEIMAN/SOS-EM" deviceLabel: HEIMAN Button manufacturer: HEIMAN diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml new file mode 100644 index 0000000000..f19f19da57 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-double-buttons-mode.yml @@ -0,0 +1,35 @@ +name: aqara-double-buttons-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button1 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: all + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml new file mode 100644 index 0000000000..904de72281 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/profiles/aqara-single-button-mode.yml @@ -0,0 +1,17 @@ +name: aqara-single-button-mode +components: + - id: main + capabilities: + - id: button + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Button +preferences: + - preferenceId: stse.allowOperationModeChange + explicit: true diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index c54e34be42..baa03c4f34 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -19,11 +19,15 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local button_utils = require "button_utils" +local MODE = "devicemode" +local MODE_CHANGE = "stse.allowOperationModeChange" +local SUPPORTED_BUTTON = { { "pushed" }, { "pushed", "held", "double" } } local PowerConfiguration = clusters.PowerConfiguration local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 local MFG_CODE = 0x115F local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 @@ -34,7 +38,9 @@ local FINGERPRINTS = { ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1 }, ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 } + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 }, + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1 }, + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3 } } local configuration = { @@ -103,6 +109,36 @@ local function battery_level_handler(driver, device, value, zb_rx) end end +local function mode_switching_handler(driver, device, value, zb_rx) + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local allow = device.preferences[MODE_CHANGE] or false + if allow then + local mode = device:get_field(MODE) or 1 + mode = 3 - mode + device:set_field(MODE, mode, { persist = true }) + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode)) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, + capabilities.button.button.pushed({ state_change = false })) + if btn_evt_cnt > 1 then + for i = 1, btn_evt_cnt do + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], + { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.numberOfButtons({ value = 1 })) + device:emit_component_event(device.profile.components[COMP_LIST[i]], + capabilities.button.button.pushed({ state_change = false })) + button_utils.emit_event_if_latest_state_missing(device, COMP_LIST[i], capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + end + end + end +end + local is_aqara_products = function(opts, driver, device) local isAqaraProducts = false if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then @@ -122,8 +158,18 @@ end local function added_handler(self, device) local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 - - device:emit_event(capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + local mode = device:get_field(MODE) or 0 + local model = device:get_model() + + if mode == 0 then + if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then + mode = 1 + else + mode = 2 + end + end + device:set_field(MODE, mode, { persist = true }) + device:emit_event(capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_event(capabilities.button.numberOfButtons({ value = 1 })) button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, @@ -135,7 +181,7 @@ local function added_handler(self, device) if btn_evt_cnt > 1 then for i = 1, btn_evt_cnt do device:emit_component_event(device.profile.components[COMP_LIST[i]], - capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, + capabilities.button.supportedButtonValues(SUPPORTED_BUTTON[mode], { visibility = { displayed = false } })) device:emit_component_event(device.profile.components[COMP_LIST[i]], capabilities.button.numberOfButtons({ value = 1 })) @@ -179,6 +225,9 @@ local aqara_wireless_switch_handler = { }, [PowerConfiguration.ID] = { [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_level_handler + }, + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_ATTRIBUTE_ID_ALIVE] = mode_switching_handler } } }, 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 1d7b6d090c..6996f05096 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -28,6 +28,8 @@ local MFG_CODE = 0x115F local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID_T1 = 0x0009 local PRIVATE_ATTRIBUTE_ID_E1 = 0x0125 +local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 +local MODE_CHANGE = "stse.allowOperationModeChange" local COMP_LIST = { "button1", "button2", "all" } local mock_device_e1 = test.mock_device.build_test_zigbee_device( @@ -44,9 +46,9 @@ local mock_device_e1 = test.mock_device.build_test_zigbee_device( } ) -local mock_device_t1_double_rocker = test.mock_device.build_test_zigbee_device( +local mock_device_h1_double_rocker = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("aqara-double-buttons.yml"), + profile = t_utils.get_profile_definition("aqara-double-buttons-mode.yml"), zigbee_endpoints = { [1] = { id = 1, @@ -61,7 +63,7 @@ local mock_device_t1_double_rocker = 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_e1) - test.mock_device.add_test_device(mock_device_t1_double_rocker) + test.mock_device.add_test_device(mock_device_h1_double_rocker) end test.set_test_init_function(test_init) @@ -69,28 +71,27 @@ test.set_test_init_function(test_init) test.register_coroutine_test( "Handle added lifecycle - T1 double rocker", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "added" }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.battery.normal())) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.type("CR2032"))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", capabilities.batteryLevel.quantity(1))) for i = 1, 3 do - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.numberOfButtons({ value = 1 }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message(COMP_LIST[i], + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], capabilities.button.button.pushed({ state_change = false }))) - end - + end end ) @@ -118,8 +119,8 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_e1.id, cluster_base.write_manufacturer_specific_attribute(mock_device_e1, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, MFG_CODE, - data_types.Uint8, 2) }) - mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 2) }) + mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -127,31 +128,31 @@ test.register_coroutine_test( test.register_coroutine_test( "Handle doConfigure lifecycle -- t1", function() - test.socket.device_lifecycle:__queue_receive({ mock_device_t1_double_rocker.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_double_rocker.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_t1_double_rocker, 30, 3600, 1) + mock_device_h1_double_rocker.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device_h1_double_rocker, 30, 3600, 1) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_bind_request(mock_device_t1_double_rocker, zigbee_test_utils.mock_hub_eui, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_bind_request(mock_device_h1_double_rocker, zigbee_test_utils.mock_hub_eui, MULTISTATE_INPUT_CLUSTER_ID) }) test.socket.zigbee:__expect_send({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attr_config(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attr_config(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, PRESENT_ATTRIBUTE_ID, 0x0003, 0x1C20, data_types.Uint16, 0x0001) }) - test.socket.zigbee:__expect_send({ mock_device_t1_double_rocker.id, - cluster_base.write_manufacturer_specific_attribute(mock_device_t1_double_rocker, PRIVATE_CLUSTER_ID, + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, + cluster_base.write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_T1, MFG_CODE, - data_types.Uint8, 1) }) - mock_device_t1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + data_types.Uint8, 1) }) + mock_device_h1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -162,14 +163,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0001 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.pushed({state_change = true}))) end ) @@ -180,14 +181,14 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0002 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.double({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.double({ state_change = true }))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", - capabilities.button.button.double({state_change = true}))) end ) @@ -198,13 +199,13 @@ test.register_coroutine_test( { PRESENT_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } } test.socket.zigbee:__queue_receive({ - mock_device_t1_double_rocker.id, - zigbee_test_utils.build_attribute_report(mock_device_t1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, MULTISTATE_INPUT_CLUSTER_ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("main", - capabilities.button.button.held({state_change = true}))) - test.socket.capability:__expect_send(mock_device_t1_double_rocker:generate_test_message("button1", + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.held({ state_change = true }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("button1", capabilities.button.button.held({ state_change = true }))) end ) @@ -254,4 +255,45 @@ test.register_message_test( } } ) + +test.register_coroutine_test( + "Wireless Remote Switch H1 Mode Change", + function() + local mode = 2 + local updates = { + preferences = { + [MODE_CHANGE] = true + } + } + test.socket.device_lifecycle:__queue_receive(mock_device_h1_double_rocker:generate_info_changed(updates)) + mock_device_h1_double_rocker:set_field("devicemode", 1, { persist = true }) + local attr_report_data = { + { PRIVATE_ATTRIBUTE_ID_ALIVE, data_types.OctetString.ID, "\x01\x21\xB8\x0B\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x45\x08\x06\x24\x07\x00\x00\x00\x00\x08\x21\x15\x01\x0A\x21\xF5\x65\x0C\x20\x01\x64\x20\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00" } + } + test.socket.zigbee:__queue_receive({ + mock_device_h1_double_rocker.id, + zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, attr_report_data, + MFG_CODE) + }) + test.socket.zigbee:__expect_send({ mock_device_h1_double_rocker.id, cluster_base + .write_manufacturer_specific_attribute(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID_E1, + MFG_CODE, data_types.Uint8, mode) }) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + + for i = 1, 3 do + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.supportedButtonValues({ "pushed", "held", "double" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_double_rocker:generate_test_message(COMP_LIST[i], + capabilities.button.button.pushed({ state_change = false }))) + end + end +) + test.run_registered_tests() From 2994e5eaa2b2217e7b00125cf0fd9e58b01b3dd2 Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Tue, 18 Nov 2025 20:30:25 +0100 Subject: [PATCH 270/449] WWSTCERT-8756 Add support to frient air quality sensor (#2439) * added support to frient air quality sensor * delete device:add_monitored_attribute from the driver * remove unused variable * change profile * removed unused health concern value * removed log statements --- .../zigbee-humidity-sensor/fingerprints.yml | 5 + ...irquality-humidity-temperature-battery.yml | 52 +++ .../src/configurations.lua | 3 +- .../src/frient-sensor/air-quality/init.lua | 156 +++++++++ .../src/frient-sensor/init.lua | 6 +- .../test/test_frient_air_quality_sensor.lua | 305 ++++++++++++++++++ 6 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index 5096f9cb25..68359710fe 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -58,6 +58,11 @@ zigbeeManufacturer: manufacturer: HEIMAN model: HT-EF-3.0 deviceProfileName: humidity-temp-battery + - id: frient/AQSZB-110 + deviceLabel: Air Quality Sensor + manufacturer: frient A/S + model: AQSZB-110 + deviceProfileName: frient-airquality-humidity-temperature-battery - id: frient/HMSZB-110 deviceLabel: frient Humidity Sensor manufacturer: frient A/S 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 new file mode 100644 index 0000000000..08774c0d31 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml @@ -0,0 +1,52 @@ +name: frient-airquality-humidity-temperature-battery +components: +- id: main + capabilities: + - id: airQualitySensor + version: 1 + - id: tvocMeasurement + version: 1 + - id: tvocHealthConcern + version: 1 + config: + values: + - key: "tvocHealthConcern.value" + enabledValues: + - good + - moderate + - slightlyUnhealthy + - unhealthy + - veryUnhealthy + - id: relativeHumidityMeasurement + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 +preferences: + - preferenceId: humidityOffset + explicit: true + - title: "Humidity Sensitivity (%)" + name: humiditySensitivity + description: "Minimum change in humidity level to report" + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 50 + default: 3 + - preferenceId: tempOffset + explicit: true + - title: "Temperature Sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index 3a4e495e3e..b040a4f73f 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -23,7 +23,8 @@ local devices = { FRIENT_HUMIDITY_TEMP_SENSOR = { FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, - { mfr = "frient A/S", model = "HMSZB-120" } + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } }, CONFIGURATION = { { diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua new file mode 100644 index 0000000000..e33c1b07e7 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua @@ -0,0 +1,156 @@ +local capabilities = require "st.capabilities" +local util = require "st.utils" +local data_types = require "st.zigbee.data_types" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local HumidityMeasurement = zcl_clusters.RelativeHumidity +local PowerConfiguration = zcl_clusters.PowerConfiguration +local device_management = require "st.zigbee.device_management" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local configurationMap = require "configurations" + +local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" } +} + +local function can_handle_frient(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then + return true + end + end + return false +end + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local MAX_VOC_REPORTABLE_VALUE = 5500 -- Max VOC reportable value + +--- Table to map VOC (ppb) to HealthConcern +local VOC_TO_HEALTHCONCERN_MAPPING = { + [2201] = "veryUnhealthy", + [661] = "unhealthy", + [221] = "slightlyUnhealthy", + [66] = "moderate", + [0] = "good", +} + +--- Map VOC (ppb) to HealthConcern +local function voc_to_healthconcern(raw_voc) + for voc, perc in util.rkeys(VOC_TO_HEALTHCONCERN_MAPPING) do + if raw_voc >= voc then + return perc + end + end +end +--- Map VOC (ppb) to CAQI +local function voc_to_caqi(raw_voc) + if (raw_voc > 5500) then + return 100 + else + return math.floor(raw_voc*99/5500) + end +end + +-- May take around 8 minutes for the first valid VOC measurement to be reported after the device is powered on +local function voc_measure_value_attr_handler(driver, device, attr_val, zb_rx) + local voc_value = attr_val.value + if (voc_value < 65535) then -- ignore it if it's outside the limits + voc_value = util.clamp_value(voc_value, 0, MAX_VOC_REPORTABLE_VALUE) + device:emit_event(capabilities.airQualitySensor.airQuality({ value = voc_to_caqi(voc_value)})) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(voc_value))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = voc_value, unit = "ppb" })) + end +end + +-- The device sends the value of MeasuredValue to be 0x8000, which corresponds to -327.68C, until it gets the first valid measurement. Therefore we don't emit event before the value is correct. It may take up to 4 minutes +local function temperatureHandler(driver, device, attr_val, zb_rx) + local temp_value = attr_val.value + if (temp_value > -32768) then + device:emit_event(capabilities.temperatureMeasurement.temperature({ value = temp_value / 100, unit = "C" })) + end +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.airQualitySensor.airQuality(voc_to_caqi(0))) + device:emit_event(capabilities.tvocHealthConcern.tvocHealthConcern(voc_to_healthconcern(0))) + device:emit_event(capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" })) +end + +local function do_refresh(driver, device) + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(HumidityMeasurement.attributes.MeasuredValue:read(device):to_endpoint(0x26)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) + end + end +end + +local function do_configure(driver, device) + device:configure() + device:send(device_management.build_bind_request(device, Frient_VOCMeasurement.ID, driver.environment_info.hub_zigbee_eui, 0x26)) + + device:send( + cluster_base.configure_reporting( + device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + ) + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) +end + +local frient_airquality_sensor = { + NAME = "frient Air Quality Sensor", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + }, + zigbee_handlers = { + cluster = {}, + attr = { + [Frient_VOCMeasurement.ID] = { + [Frient_VOCMeasurement.attributes.MeasuredValue.ID] = voc_measure_value_attr_handler, + }, + [TemperatureMeasurement.ID] = { + [TemperatureMeasurement.attributes.MeasuredValue.ID] = temperatureHandler, + }, + } + }, + can_handle = can_handle_frient +} + +return frient_airquality_sensor \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index 7791e6752b..1356345ff2 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -20,7 +20,8 @@ local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { { mfr = "frient A/S", model = "HMSZB-110" }, - { mfr = "frient A/S", model = "HMSZB-120" } + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } } local function can_handle_frient_sensor(opts, driver, device) @@ -73,6 +74,9 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, + sub_drivers = { + require("frient-sensor/air-quality") + }, can_handle = can_handle_frient_sensor } 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 new file mode 100644 index 0000000000..607a8aa19e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua @@ -0,0 +1,305 @@ +-- Copyright 2025 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 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 data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local HumidityMeasurement = clusters.RelativeHumidity + +local Frient_VOCMeasurement = { + ID = 0xFC03, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValue = { ID = 0x0000, base_type = data_types.Uint16 }, + MinMeasuredValue = { ID = 0x0001, base_type = data_types.Uint16 }, + MaxMeasuredValue = { ID = 0x0002, base_type = data_types.Uint16 }, + Resolution = { ID = 0x0003, base_type = data_types.Uint16 }, + }, +} + +Frient_VOCMeasurement.attributes.MeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MinMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.MaxMeasuredValue._cluster = Frient_VOCMeasurement +Frient_VOCMeasurement.attributes.Resolution._cluster = Frient_VOCMeasurement + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-airquality-humidity-temperature-battery.yml"), + zigbee_endpoints = { + [0x26] = { + id = 0x26, + manufacturer = "frient A/S", + model = "AQSZB-110", + server_clusters = {0x0001, 0x0402, 0x0405, 0xFC03} + } + } + } +) + +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( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = {mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_coroutine_test( + "Configure should configure all necessary attributes", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + 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.BatteryVoltage:configure_reporting(mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + HumidityMeasurement.ID + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 60, 3600, 300) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + Frient_VOCMeasurement.ID, + 38 + ):to_endpoint(0x26) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting( + mock_device, + data_types.ClusterId(Frient_VOCMeasurement.ID), + Frient_VOCMeasurement.attributes.MeasuredValue.ID, + Frient_VOCMeasurement.attributes.MeasuredValue.base_type.ID, + 60, 600, 10 + ):to_endpoint(0x26) + }) + + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Humidity report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 0x1950) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 65 })) + } + } +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2500) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + } +) + +test.register_coroutine_test( + "info_changed to check for necessary preferences settings: Temperature Sensitivity", + function() + local updates = { + preferences = { + temperatureSensitivity = 0.9, + humiditySensitivity = 10 + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local temperatureSensitivity = math.floor(0.9 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 3600, + temperatureSensitivity + ) + }) + local humiditySensitivity = math.floor(10 * 100 + 0.5) + test.socket.zigbee:__expect_send({ mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 60, + 3600, + humiditySensitivity + ) + }) + test.wait_for_events() + end +) + +test.register_message_test( + "VOC measurement report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, cluster_base.build_test_attr_report(Frient_VOCMeasurement.attributes.MeasuredValue, mock_device, 300) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.airQualitySensor.airQuality({ value = 5 })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocHealthConcern.tvocHealthConcern({ value = "slightlyUnhealthy" })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tvocMeasurement.tvocLevel({ value = 300, unit = "ppb" })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() \ No newline at end of file From 4df2ed64c0a97cfc84cd5fb9ae2cf58359d948e3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 18 Nov 2025 16:06:26 -0800 Subject: [PATCH 271/449] WWSTCERT-9005 Aqara Light Switch H2 US (2 Buttons, 2 Channel) WWSTCERT-9008 Aqara Light Switch H2 US (4 Buttons, 3 Channel) WWSTCERT-9011 Aqara Light Switch H2 US (2 Buttons, 1 Channel) WWSTCERT-9014 Aqara Light Switch H2 EU (2 Buttons, 1 Channel) WWSTCERT-9017 Aqara Light Switch H2 EU (4 Buttons, 2 Channel) --- .../matter-switch/fingerprints.yml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index c67eec7bf2..77549a23ed 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -25,6 +25,31 @@ matterManufacturer: vendorId: 0x115F productId: 0x2004 deviceProfileName: 3-button-battery-temperature-humidity + - id: "4447/4105" + deviceLabel: Aqara Light Switch H2 EU (4 Buttons 2 Channel) + vendorId: 0x115F + productId: 0x1009 + deviceProfileName: 4-button + - id: "4447/4104" + deviceLabel: Aqara Light Switch H2 EU (2 Buttons 1 Channel) + vendorId: 0x115F + productId: 0x1008 + deviceProfileName: 2-button + - id: "4447/4099" + deviceLabel: Aqara Light Switch H2 US (2 Buttons 1 Channel) + vendorId: 0x115F + productId: 0x1003 + deviceProfileName: 2-button + - id: "4447/4100" + deviceLabel: Aqara Light Switch H2 US (2 Buttons 2 Channel) + vendorId: 0x115F + productId: 0x1004 + deviceProfileName: 2-button + - id: "4447/4101" + deviceLabel: Aqara Light Switch H2 US (4 Buttons 3 Channel) + vendorId: 0x115F + productId: 0x1005 + deviceProfileName: 4-button #AiDot - id: "Linkind/Smart/Light/Bulb/A19/RGBTW" deviceLabel: Linkind Smart Light Bulb A19 RGBTW From 713c658d70e61bb96b1fecbe0f8c65b884c3db28 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 18 Nov 2025 16:10:37 -0800 Subject: [PATCH 272/449] WWSTCERT-9021 Aqara Presence Multi-Sensor FP300 --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index c1e2f2dc04..36ec8b6bc8 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -10,6 +10,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2003 deviceProfileName: motion-illuminance-battery + - id: "4447/8197" + deviceLabel: Presence Multi-Sensor FP300 + vendorId: 0x115F + productId: 0x2005 + deviceProfileName: motion-illuminance-temperature-humidity-batteryLevel #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" From e628b473e4b26e3c7deeee4b3132928a2ce2008e Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:08:19 -0600 Subject: [PATCH 273/449] Matter Window Covering: Remove default tilt preset (#2565) Remove the default tilt preset level of 50%. --- .../matter-window-covering/src/init.lua | 29 ++++++------------- .../src/test/test_matter_window_covering.lua | 3 -- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 289c164e64..6759560c50 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -29,7 +29,7 @@ local battery_support = { } local REVERSE_POLARITY = "__reverse_polarity" local PRESET_LEVEL_KEY = "__preset_level_key" -local PRESET_LEVEL = 50 +local DEFAULT_PRESET_LEVEL = 50 local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -78,7 +78,7 @@ local function device_init(driver, device) 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 - PRESET_LEVEL + 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 @@ -131,25 +131,14 @@ 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 endpoint_id = device:component_to_endpoint(cmd.component) - local lift_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) - local tilt_eps = device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) - if #lift_eps > 0 then - local lift_value = device:get_latest_state( - "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME - ) or PRESET_LEVEL - local hundredths_lift_percent = (100 - lift_value) * 100 - local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( - device, endpoint_id, hundredths_lift_percent - ) - device:send(req) - end - if #tilt_eps > 0 then - local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( - device, endpoint_id, PRESET_LEVEL * 100 - ) - device:send(req) - end + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percent + )) end local function handle_set_preset(driver, device, cmd) 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 b30f2ea4bf..409ebfcb09 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 @@ -789,9 +789,6 @@ test.register_coroutine_test( test.socket.matter:__expect_send( {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, (100 - PRESET_LEVEL) * 100)} ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToTiltPercentage(mock_device, 10, 5000)} - ) end ) From b828a5a984312da95f456241b00a5c1ffef90178 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:29:56 -0600 Subject: [PATCH 274/449] Matter Switch: Add more reliability to device type checking (#2564) --- .../src/switch_utils/device_configuration.lua | 2 +- .../matter-switch/src/switch_utils/fields.lua | 1 + .../matter-switch/src/switch_utils/utils.lua | 36 ++++++++++++------- 3 files changed, 25 insertions(+), 14 deletions(-) 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 adc0da500e..eebe734fcd 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -25,7 +25,7 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on -- per spec, the Switch device types support OnOff as CLIENT, though some vendors break spec and support it as SERVER. local primary_dt_id = switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.LIGHT) or switch_utils.find_max_subset_device_type(ep_info, fields.DEVICE_TYPE_ID.SWITCH) - or ep_info.device_types[1] and ep_info.device_types[1].device_type_id + or switch_utils.find_primary_device_type(ep_info) local generic_profile = fields.device_type_profile_map[primary_dt_id] diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index c350a7adaf..3a0bdc9fdb 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -36,6 +36,7 @@ SwitchFields.CURRENT_HUESAT_ATTR_MAX = 254 SwitchFields.DEVICE_TYPE_ID = { AGGREGATOR = 0x000E, + BRIDGED_NODE = 0x0013, CAMERA = 0x0142, CHIME = 0x0146, DIMMABLE_PLUG_IN_UNIT = 0x010B, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 718e186dcf..01a9d2d971 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -93,23 +93,33 @@ function utils.device_type_supports_button_switch_combination(device, endpoint_i return utils.tbl_contains(dimmable_eps, endpoint_id) end --- Some devices report multiple device types which are a subset of --- a superset device type (Ex. Dimmable Light is a superset of On/Off Light). --- We should map to the largest superset device type supported. --- This can be done by matching to the device type with the highest ID +--- Some devices report multiple device types which are a subset of a superset +--- device type (Ex. Dimmable Light is a superset of On/Off Light). We should map +--- to the largest superset device type supported. +--- This can be done by matching to the device type with the highest ID +--- note: that superset device types have a higher ID than those of their subset +--- is heuristic and could therefore break in the future, were the spec expanded function utils.find_max_subset_device_type(ep, device_type_set) if ep.endpoint_id == 0 then return end -- EP-scoped device types not permitted on Root Node - local primary_dt_id = ep.device_types[1] and ep.device_types[1].device_type_id - if utils.tbl_contains(device_type_set, primary_dt_id) then - for _, dt in ipairs(ep.device_types) do - -- only device types in the subset should be considered. - if utils.tbl_contains(device_type_set, dt.device_type_id) then - primary_dt_id = math.max(primary_dt_id, dt.device_type_id) - end + local primary_dt_id = -1 + for _, dt in ipairs(ep.device_types) do + -- only device types in the subset should be considered. + if utils.tbl_contains(device_type_set, dt.device_type_id) then + primary_dt_id = math.max(primary_dt_id, dt.device_type_id) + end + end + return (primary_dt_id > 0) and primary_dt_id or nil +end + +--- Lights and Switches are Device Types that have Superset-style functionality +--- For all other device types, this function should be used to identify the primary device type +function utils.find_primary_device_type(ep_info) + for _, dt in ipairs(ep_info.device_types) do + if dt.device_type_id ~= fields.DEVICE_TYPE_ID.BRIDGED_NODE then + -- if this is not a bridged node, return the first device type seen + return dt.device_type_id end - return primary_dt_id end - return nil end --- find_default_endpoint is a helper function to handle situations where From 1395f676f41755ef2ba1ec829fd9778f31f6f2ba Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:03:50 -0600 Subject: [PATCH 275/449] Fix webrtc capability inclusion requirements (#2568) In the matter camera subdriver, the webrtc capability should be included if the `WebRTCTransportProvider` clusters is implemented as `SERVER` and if `WebRTCTransportRequestor` is implemented as `CLIENT`; this commit adds a check for the former which is currently missing from the subdriver. --- .../camera/camera_utils/device_configuration.lua | 7 ++++--- .../matter-switch/src/test/test_matter_camera.lua | 2 +- 2 files changed, 5 insertions(+), 4 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 80ac3be711..e307db8d15 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 @@ -51,6 +51,10 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) if #camera_endpoints > 0 then + if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) + end local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) for _, ep_cluster in pairs(camera_ep.clusters or {}) do if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID then @@ -102,9 +106,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.zoneManagement.ID) elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID then table.insert(main_component_capabilities, capabilities.motionSensor.ID) - elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and - #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then - table.insert(main_component_capabilities, capabilities.webrtc.ID) end end 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 b9804a2a76..d504107c79 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -156,6 +156,7 @@ local function update_device_profile() { "main", { + "webrtc", "videoCapture2", "cameraViewportSettings", "localMediaStorage", @@ -167,7 +168,6 @@ local function update_device_profile() "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", - "webrtc", "motionSensor", "sounds", } From 16bd6225afabba7fa33e4ed756ec80063bae6887 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:14:31 -0600 Subject: [PATCH 276/449] Fix webrtc capability inclusion requirements (#2568) (#2571) Co-authored-by: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> --- .../camera/camera_utils/device_configuration.lua | 7 ++++--- .../matter-switch/src/test/test_matter_camera.lua | 2 +- 2 files changed, 5 insertions(+), 4 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 80ac3be711..e307db8d15 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 @@ -51,6 +51,10 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) if #camera_endpoints > 0 then + if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) + end local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) for _, ep_cluster in pairs(camera_ep.clusters or {}) do if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID then @@ -102,9 +106,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.zoneManagement.ID) elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID then table.insert(main_component_capabilities, capabilities.motionSensor.ID) - elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and - #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then - table.insert(main_component_capabilities, capabilities.webrtc.ID) end end 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 b9804a2a76..d504107c79 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -156,6 +156,7 @@ local function update_device_profile() { "main", { + "webrtc", "videoCapture2", "cameraViewportSettings", "localMediaStorage", @@ -167,7 +168,6 @@ local function update_device_profile() "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", - "webrtc", "motionSensor", "sounds", } From 2389712bd840942d1d641a89b3ef4e0b48efefe6 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 24 Nov 2025 10:22:16 -0600 Subject: [PATCH 277/449] Matter Camera: Check for server cluster type (#2570) Check for cluster_type = SERVER or BOTH when adding optional capabilities. When using get_endpoints, omit the check for SERVER because it is already implied in that api. --- .../camera_utils/device_configuration.lua | 19 +++++++++++-------- .../src/test/test_matter_camera.lua | 2 +- 2 files changed, 12 insertions(+), 9 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 e307db8d15..90c5ee3498 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 @@ -49,15 +49,15 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local microphone_component_capabilities = {} local doorbell_component_capabilities = {} + local function has_server_cluster_type(cluster) + return cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH" + end + local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) if #camera_endpoints > 0 then - if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 and - #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then - table.insert(main_component_capabilities, capabilities.webrtc.ID) - end local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) for _, ep_cluster in pairs(camera_ep.clusters or {}) do - if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID then + if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID and has_server_cluster_type(ep_cluster) then local clus_has_feature = function(feature_bitmap) return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) end @@ -92,7 +92,7 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.NIGHT_VISION) then table.insert(main_component_capabilities, capabilities.nightVision.ID) end - elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID then + elseif ep_cluster.cluster_id == clusters.CameraAvSettingsUserLevelManagement.ID and has_server_cluster_type(ep_cluster) then local clus_has_feature = function(feature_bitmap) return clusters.CameraAvSettingsUserLevelManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) end @@ -102,10 +102,13 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) end table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) - elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID then + 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.OccupancySensing.ID then + 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) and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) end end 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 d504107c79..b9804a2a76 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -156,7 +156,6 @@ local function update_device_profile() { "main", { - "webrtc", "videoCapture2", "cameraViewportSettings", "localMediaStorage", @@ -168,6 +167,7 @@ local function update_device_profile() "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", + "webrtc", "motionSensor", "sounds", } From d957e692f1ee0936eabc2d34bd00305ec3830354 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:28:36 -0600 Subject: [PATCH 278/449] Matter Switch: Simplify component to endpoint mapping logic (#2560) --- .../camera_handlers/attribute_handlers.lua | 5 +- .../sub_drivers/camera/camera_utils/utils.lua | 8 --- .../matter-switch/src/switch_utils/utils.lua | 53 ++++++------------- 3 files changed, 17 insertions(+), 49 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 0e3fe5f843..484e4c7a15 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 @@ -396,15 +396,13 @@ end function CameraAttributeHandlers.camera_av_stream_management_attribute_list_handler(driver, device, ib, response) if not ib.data.elements then return end local status_light_enabled_present, status_light_brightness_present = false, false - local attribute_ids, capability_ids = {}, {} + local attribute_ids = {} for _, attr in ipairs(ib.data.elements) do if attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID then status_light_enabled_present = true - table.insert(capability_ids, capabilities.switch.ID) table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) elseif attr.value == clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID then status_light_brightness_present = true - table.insert(capability_ids, capabilities.mode.ID) table.insert(attribute_ids, clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) end end @@ -413,7 +411,6 @@ function CameraAttributeHandlers.camera_av_stream_management_attribute_list_hand endpoint_id = ib.endpoint_id, cluster_id = ib.cluster_id, attribute_ids = attribute_ids, - capability_ids = capability_ids } device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist=true}) camera_cfg.match_profile(device, status_light_enabled_present, status_light_brightness_present) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 4334d2a304..dcdd936950 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -35,10 +35,6 @@ function CameraUtils.update_camera_component_map(device) clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel.ID, clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel.ID, }, - capability_ids = { - capabilities.audioMute.ID, - capabilities.audioVolume.ID, - } } end if CameraUtils.feature_supported(device, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.types.Feature.VIDEO) then @@ -51,10 +47,6 @@ function CameraUtils.update_camera_component_map(device) clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel.ID, clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel.ID, }, - capability_ids = { - capabilities.audioMute.ID, - capabilities.audioVolume.ID, - } } end device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 01a9d2d971..b6cc09e007 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -178,60 +178,40 @@ function utils.component_to_endpoint(device, component) return utils.find_default_endpoint(device) end ---- An extension of the library function endpoint_to_component, to support a mapping scheme ---- that includes cluster and attribute id's so that we can use multiple components for a ---- single endpoint. +--- An extension of the library function endpoint_to_component, used to support a mapping scheme +--- that optionally includes cluster and attribute ids so that multiple components can be mapped +--- to a single endpoint. --- --- @param device any a Matter device object ---- @param opts number|table either is an ep_id or a table { endpoint_id, capability_id } +--- @param ep_info number|table either an ep_id or a table { endpoint_id, optional(cluster_id), optional(attribute_id) } +--- where cluster_id is required for an attribute_id to be handled. --- @return string component -function utils.endpoint_to_component(device, opts) - local ep_info = {} - if type(opts) == "number" then - ep_info.endpoint_id = opts - elseif type(opts) == "table" then - if opts.endpoint_info then - ep_info = opts.endpoint_info - else - ep_info = { - endpoint_id = opts.endpoint_id, - cluster_id = opts.cluster_id, - attribute_id = opts.attribute_id - } - end +function utils.endpoint_to_component(device, ep_info) + if type(ep_info) == "number" then + ep_info = { endpoint_id = ep_info } end for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do if type(map_info) == "number" and map_info == ep_info.endpoint_id then return component - elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id then - if (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id - and utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id))) - and (not opts.capability_id or utils.tbl_contains(map_info.capability_ids, opts.capability_id)) then + elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id + and (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id + and (not map_info.attribute_ids or utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id)))) then return component - end end end return "main" end ---- An extension of the library function emit_event_for_endpoint, to support devices with ---- multiple components defined for the same endpoint, since they can't be easily ---- differentiated based on a simple endpoint id to component mapping, but we can extend ---- this mapping to include the cluster and attribute id's so that we know which component ---- to route events to. +--- An extension of the library function emit_event_for_endpoint, used to support devices with +--- multiple components mapped to the same endpoint. This is handled by extending the parameters to optionally +--- include a cluster id and attribute id for more specific routing --- --- @param device any a Matter device object ---- @param ep_info number|table endpoint_id or ib (includes endpoint_id, cluster_id, attribute_id) +--- @param ep_info number|table endpoint_id or an ib (the ib data includes endpoint_id, cluster_id, and attribute_id fields) --- @param event any a capability event object function utils.emit_event_for_endpoint(device, ep_info, event) if type(ep_info) == "number" then ep_info = { endpoint_id = ep_info } - elseif type(ep_info) == "table" then - ep_info = { - endpoint_id = ep_info.endpoint_id, - cluster_id = ep_info.cluster_id, - attribute_id = ep_info.attribute_id - } end if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then local child = utils.find_child(device, ep_info.endpoint_id) @@ -240,8 +220,7 @@ function utils.emit_event_for_endpoint(device, ep_info, event) return end end - local opts = { endpoint_info = ep_info, capability_id = event.capability.ID } - local comp_id = utils.endpoint_to_component(device, opts) + local comp_id = utils.endpoint_to_component(device, ep_info) local comp = device.profile.components[comp_id] device:emit_component_event(comp, event) end From 5ca5f9c40ac33d2bc11e09773505509570983e47 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 24 Nov 2025 11:33:36 -0800 Subject: [PATCH 279/449] WWSTCERT-8445 Kwikset Halo Select Plus (#2498) * WWSTCERT-8445 Kwikset Halo Select Plus * add to new matter lock --- drivers/SmartThings/matter-lock/fingerprints.yml | 6 ++++++ .../SmartThings/matter-lock/src/new-matter-lock/init.lua | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 27722d9207..bd0dbcdf3e 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -51,6 +51,12 @@ matterManufacturer: vendorId: 0x1533 productId: 0x0012 deviceProfileName: lock-user-pin-battery + #Kwikset + - id: "5153/66" + deviceLabel: Kwikset Halo Select Plus + vendorId: 0x1421 + productId: 0x0042 + deviceProfileName: lock-user-pin-battery #Level - id: "4767/1" deviceLabel: Level Lock Plus (Matter) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index cbab5a4db0..0eee7edc40 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -82,7 +82,8 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x135D, 0x00B0}, -- Nuki, Smart Lock {0x15F2, 0x0001}, -- Viomi, AiSafety Smart Lock E100 {0x158B, 0x0001}, -- Deasino, DS-MT01 - {0x10E1, 0x2002} -- VDA + {0x10E1, 0x2002}, -- VDA + {0x1421, 0x0042}, -- Kwikset Halo Select Plus } local battery_support = { From 4de04b6d51fe67deef04a2ea23126a9eb3249aef Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 24 Nov 2025 11:41:11 -0800 Subject: [PATCH 280/449] WWSTCERT-9051 eufy FamiLock E32 --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 2 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index bd0dbcdf3e..6c7a223caf 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -51,6 +51,11 @@ matterManufacturer: vendorId: 0x1533 productId: 0x0012 deviceProfileName: lock-user-pin-battery + - id: "5427/22" + deviceLabel: eufy FamiLock E32 + vendorId: 0x1533 + productId: 0x0016 + deviceProfileName: lock-user-pin-battery #Kwikset - id: "5153/66" deviceLabel: Kwikset Halo Select Plus diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 0eee7edc40..1d59b1ab14 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -75,6 +75,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x1533, 0x0010}, -- eufy, FamiLock S3 {0x1533, 0x0011}, -- eufy, FamiLock E34 {0x1533, 0x0012}, -- eufy, FamiLock E35 + {0x1533, 0x0016}, -- eufy, FamiLock E32 {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro {0x135D, 0x00B2}, -- Nuki, Smart Lock {0x135D, 0x00C1}, -- Nuki, Smart Lock From d7460ea07ab2acdb0ce19968f8a0745e981955fc Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 24 Nov 2025 11:44:07 -0800 Subject: [PATCH 281/449] WWSTCERT-9074 Cync Fan Switch --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 8358e3164e..92a8ea3207 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -16,6 +16,12 @@ matterManufacturer: vendorId: 0x1209 productId: 0x3012 deviceProfileName: thermostat-heating-only-nostate + #Cync + - id: "4921/121" + deviceLabel: Cync Fan Switch + vendorId: 0x1339 + productId: 0x0079 + deviceProfileName: fan-generic #Eve - id: "4874/79" deviceLabel: Eve Thermo From 2e7ffb96480bfa7210f05d9024f2898f9d707d65 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:35:36 -0600 Subject: [PATCH 282/449] reorder the matter thermostat directory structure (#2548) --- .../server/attributes/AttributeList.lua | 76 - .../attributes/CumulativeEnergyExported.lua | 68 - .../attributes/PeriodicEnergyExported.lua | 68 - .../events/CumulativeEnergyMeasured.lua | 108 - .../server/events/PeriodicEnergyMeasured.lua | 108 - .../server/events/init.lua | 25 - .../types/Feature.lua | 116 - .../src/ElectricalPowerMeasurement/init.lua | 94 - .../types/Feature.lua | 138 - .../ElectricalPowerMeasurement/types/init.lua | 15 - .../server/attributes/AttributeList.lua | 75 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/LevelValue.lua | 41 - .../server/attributes/MeasuredValue.lua | 56 - .../server/attributes/MeasurementUnit.lua | 45 - .../server/attributes/AcceptedCommandList.lua | 75 - .../ActivatedCarbonFilterMonitoring/init.lua | 9 +- .../server/attributes/ChangeIndication.lua | 5 +- .../server/attributes/Condition.lua | 5 +- .../server/attributes/init.lua | 5 +- .../server/commands/ResetCondition.lua | 3 + .../server/commands/init.lua | 5 +- .../types/ChangeIndicationEnum.lua | 3 + .../types/Feature.lua | 3 + .../types/init.lua | 5 +- .../AirQuality/init.lua | 7 +- .../server/attributes/AcceptedCommandList.lua | 3 + .../server/attributes/AirQuality.lua | 5 +- .../server/attributes/AttributeList.lua | 3 + .../server/attributes/EventList.lua | 3 + .../AirQuality/server/attributes/init.lua | 5 +- .../AirQuality/types/AirQualityEnum.lua | 3 + .../AirQuality/types/Feature.lua | 3 + .../AirQuality/types/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 5 +- .../server/attributes/MeasuredValue.lua | 3 + .../server/attributes/MeasurementUnit.lua | 5 +- .../server/attributes/init.lua | 5 +- .../types/Feature.lua | 3 + .../types/LevelValueEnum.lua | 3 + .../types/MeasurementUnitEnum.lua | 3 + .../ConcentrationMeasurement/types/init.lua | 5 +- .../ElectricalEnergyMeasurement/init.lua | 30 +- .../attributes/CumulativeEnergyImported.lua | 5 +- .../attributes/PeriodicEnergyImported.lua | 5 +- .../server/attributes/init.lua | 5 +- .../types/EnergyMeasurementStruct.lua | 3 + .../types/Feature.lua | 31 + .../types/init.lua | 5 +- .../ElectricalPowerMeasurement/init.lua | 44 + .../server/attributes/ActivePower.lua | 4 +- .../server/attributes/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../HepaFilterMonitoring/init.lua | 9 +- .../server/attributes/ChangeIndication.lua | 5 +- .../server/attributes/Condition.lua | 3 + .../server/attributes/init.lua | 5 +- .../server/commands/ResetCondition.lua | 3 + .../server/commands/init.lua | 5 +- .../types/ChangeIndicationEnum.lua | 3 + .../HepaFilterMonitoring/types/Feature.lua | 3 + .../HepaFilterMonitoring/types/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 7 +- .../server/attributes/MeasuredValue.lua | 5 +- .../server/attributes/MeasurementUnit.lua | 7 +- .../server/attributes/init.lua | 5 +- .../OzoneConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../Pm10ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../Pm1ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../Pm25ConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../RadonConcentrationMeasurement/init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../init.lua | 7 +- .../server/attributes/LevelValue.lua | 44 + .../server/attributes/MeasuredValue.lua | 59 + .../server/attributes/MeasurementUnit.lua | 48 + .../server/attributes/init.lua | 5 +- .../WaterHeaterMode/init.lua | 9 +- .../server/attributes/CurrentMode.lua | 4 +- .../server/attributes/SupportedModes.lua | 5 +- .../server/attributes/init.lua | 5 +- .../server/commands/ChangeToMode.lua | 3 + .../WaterHeaterMode/server/commands/init.lua | 5 +- .../WaterHeaterMode/types/Feature.lua | 3 + .../types/ModeOptionStruct.lua | 5 +- .../WaterHeaterMode/types/ModeTag.lua | 3 + .../WaterHeaterMode/types/ModeTagStruct.lua | 3 + .../WaterHeaterMode/types/init.lua | 5 +- .../matter-thermostat/src/init.lua | 2563 +++-------------- .../src/test/test_matter_air_purifier.lua | 42 +- .../test/test_matter_air_purifier_api9.lua | 42 +- .../test/test_matter_air_purifier_modular.lua | 45 +- .../src/test/test_matter_fan.lua | 16 +- .../src/test/test_matter_heat_pump.lua | 19 +- .../src/test/test_matter_room_ac.lua | 16 +- .../src/test/test_matter_room_ac_modular.lua | 16 +- .../src/test/test_matter_thermo_battery.lua | 15 +- .../test/test_matter_thermo_featuremap.lua | 15 +- ...st_matter_thermo_multiple_device_types.lua | 15 +- .../test_matter_thermo_setpoint_limits.lua | 15 +- ...test_matter_thermo_setpoint_limits_rpc.lua | 14 +- .../src/test/test_matter_thermostat.lua | 15 +- ...est_matter_thermostat_composed_bridged.lua | 15 +- .../test/test_matter_thermostat_modular.lua | 15 +- .../src/test/test_matter_thermostat_rpc5.lua | 15 +- .../src/test/test_matter_water_heater.lua | 20 +- .../attribute_handlers.lua | 661 +++++ .../capability_handlers.lua | 256 ++ .../thermostat_utils/device_configuration.lua | 324 +++ .../embedded_cluster_utils.lua} | 37 +- .../src/thermostat_utils/fields.lua | 238 ++ .../legacy_device_configuration.lua | 259 ++ .../src/thermostat_utils/utils.lua | 183 ++ 162 files changed, 3764 insertions(+), 4374 deletions(-) delete mode 100644 drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua delete mode 100644 drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/init.lua (90%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{HepaFilterMonitoring => embedded_clusters/ActivatedCarbonFilterMonitoring}/server/commands/ResetCondition.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/server/commands/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua (92%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/types/Feature.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ActivatedCarbonFilterMonitoring/types/init.lua (62%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/init.lua (86%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/server/attributes/AcceptedCommandList.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/server/attributes/AirQuality.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/server/attributes/AttributeList.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/server/attributes/EventList.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/server/attributes/init.lua (75%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/types/AirQualityEnum.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/types/Feature.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/AirQuality/types/init.lua (60%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonDioxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{CarbonMonoxideConcentrationMeasurement => embedded_clusters/CarbonDioxideConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-thermostat/src/{NitrogenDioxideConcentrationMeasurement => embedded_clusters/CarbonDioxideConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{CarbonDioxideConcentrationMeasurement => embedded_clusters/CarbonMonoxideConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/init.lua (92%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/LevelValue.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/MeasuredValue.lua (93%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/types/Feature.lua (98%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/types/LevelValueEnum.lua (93%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/types/MeasurementUnitEnum.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ConcentrationMeasurement/types/init.lua (62%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/init.lua (61%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua (97%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalEnergyMeasurement/types/init.lua (62%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalPowerMeasurement/server/attributes/ActivePower.lua (93%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/ElectricalPowerMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{NitrogenDioxideConcentrationMeasurement => embedded_clusters/FormaldehydeConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-thermostat/src/{CarbonDioxideConcentrationMeasurement => embedded_clusters/FormaldehydeConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/FormaldehydeConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/init.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/server/attributes/ChangeIndication.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/server/attributes/Condition.lua (93%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ActivatedCarbonFilterMonitoring => embedded_clusters/HepaFilterMonitoring}/server/commands/ResetCondition.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/server/commands/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/types/ChangeIndicationEnum.lua (92%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/types/Feature.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/HepaFilterMonitoring/types/init.lua (61%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/init.lua (88%) rename drivers/SmartThings/matter-thermostat/src/{FormaldehydeConcentrationMeasurement => embedded_clusters/NitrogenDioxideConcentrationMeasurement}/server/attributes/LevelValue.lua (81%) rename drivers/SmartThings/matter-thermostat/src/{FormaldehydeConcentrationMeasurement => embedded_clusters/NitrogenDioxideConcentrationMeasurement}/server/attributes/MeasuredValue.lua (89%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua (82%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/OzoneConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/OzoneConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm10ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm10ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm1ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm1ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm25ConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/Pm25ConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/RadonConcentrationMeasurement/init.lua (85%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/RadonConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua (89%) create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/init.lua (87%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/server/attributes/CurrentMode.lua (93%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/server/attributes/SupportedModes.lua (90%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/server/attributes/init.lua (75%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/server/commands/ChangeToMode.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/server/commands/init.lua (76%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/types/Feature.lua (92%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/types/ModeOptionStruct.lua (94%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/types/ModeTag.lua (90%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/types/ModeTagStruct.lua (96%) rename drivers/SmartThings/matter-thermostat/src/{ => embedded_clusters}/WaterHeaterMode/types/init.lua (61%) create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua rename drivers/SmartThings/matter-thermostat/src/{embedded-cluster-utils.lua => thermostat_utils/embedded_cluster_utils.lua} (67%) create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua create mode 100644 drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index 320826dacc..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,76 +0,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 AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList: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, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList: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 AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua deleted file mode 100644 index 22befec642..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyExported.lua +++ /dev/null @@ -1,68 +0,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 CumulativeEnergyExported = { - ID = 0x0002, - NAME = "CumulativeEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function CumulativeEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function CumulativeEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function CumulativeEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function CumulativeEnergyExported: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 CumulativeEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(CumulativeEnergyExported, {__call = CumulativeEnergyExported.new_value, __index = CumulativeEnergyExported.base_type}) -return CumulativeEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua deleted file mode 100644 index 4c1ee29274..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyExported.lua +++ /dev/null @@ -1,68 +0,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 PeriodicEnergyExported = { - ID = 0x0004, - NAME = "PeriodicEnergyExported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", -} - -function PeriodicEnergyExported:new_value(...) - local o = self.base_type(table.unpack({...})) - self:augment_type(o) - return o -end - -function PeriodicEnergyExported:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function PeriodicEnergyExported:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function PeriodicEnergyExported: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 PeriodicEnergyExported:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(PeriodicEnergyExported, {__call = PeriodicEnergyExported.new_value, __index = PeriodicEnergyExported.base_type}) -return PeriodicEnergyExported - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua deleted file mode 100644 index 13136bd791..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/CumulativeEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local CumulativeEnergyMeasured = { - ID = 0x0000, - NAME = "CumulativeEnergyMeasured", - base_type = data_types.Structure, -} - -CumulativeEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function CumulativeEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - 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 - base_type_obj.elements = elems -end - -function CumulativeEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function CumulativeEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "CumulativeEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.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 fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function CumulativeEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function CumulativeEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return CumulativeEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua deleted file mode 100644 index e2c4b1b577..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/PeriodicEnergyMeasured.lua +++ /dev/null @@ -1,108 +0,0 @@ -local data_types = require "st.matter.data_types" -local cluster_base = require "st.matter.cluster_base" -local TLVParser = require "st.matter.TLV.TLVParser" -local StructureABC = require "st.matter.data_types.base_defs.StructureABC" - -local PeriodicEnergyMeasured = { - ID = 0x0001, - NAME = "PeriodicEnergyMeasured", - base_type = data_types.Structure, -} - -PeriodicEnergyMeasured.field_defs = { - { - name = "energy_imported", - field_id = 0, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, - { - name = "energy_exported", - field_id = 1, - is_nullable = false, - is_optional = true, - data_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", - }, -} - -function PeriodicEnergyMeasured:augment_type(base_type_obj) - local elems = {} - for _, v in ipairs(base_type_obj.elements) do - for _, field_def in ipairs(self.field_defs) do - if field_def.field_id == v.field_id and not - ((field_def.is_nullable or field_def.is_optional) and v.elements == nil) then - elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) - 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 - base_type_obj.elements = elems -end - -function PeriodicEnergyMeasured:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - nil, --attribute_id - self.ID - ) -end - -function PeriodicEnergyMeasured:build_test_event_report( - device, - endpoint_id, - fields, - status -) - local data = {} - data.elements = {} - data.num_elements = 0 - setmetatable(data, StructureABC.new_mt({NAME = "PeriodicEnergyMeasuredEventData", ID = 0x15})) - for idx, field_def in ipairs(self.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 fields[field_def.name] then - error("Missing non optional or non_nullable field: " .. field_def.name) - elseif fields[field_def.name] then - data.elements[field_def.name] = data_types.validate_or_build_type(fields[field_def.name], field_def.data_type, field_def.name) - data.elements[field_def.name].field_id = field_def.field_id - data.num_elements = data.num_elements + 1 - end - end - return cluster_base.build_test_event_report( - device, - endpoint_id, - self._cluster.ID, - self.ID, - data, - status - ) -end - -function PeriodicEnergyMeasured:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -function PeriodicEnergyMeasured:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -return PeriodicEnergyMeasured - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua deleted file mode 100644 index 02b085583e..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/events/init.lua +++ /dev/null @@ -1,25 +0,0 @@ -local event_mt = {} -event_mt.__event_cache = {} -event_mt.__index = function(self, key) - if event_mt.__event_cache[key] == nil then - local req_loc = string.format("ElectricalEnergyMeasurement.server.events.%s", key) - local raw_def = require(req_loc) - local cluster = rawget(self, "_cluster") - raw_def:set_parent_cluster(cluster) - event_mt.__event_cache[key] = raw_def - end - return event_mt.__event_cache[key] -end - - -local ElectricalEnergyMeasurementEvents = {} - -function ElectricalEnergyMeasurementEvents:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -setmetatable(ElectricalEnergyMeasurementEvents, event_mt) - -return ElectricalEnergyMeasurementEvents - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua deleted file mode 100644 index 717ba6a2f3..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/Feature.lua +++ /dev/null @@ -1,116 +0,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.IMPORTED_ENERGY = 0x0001 -Feature.EXPORTED_ENERGY = 0x0002 -Feature.CUMULATIVE_ENERGY = 0x0004 -Feature.PERIODIC_ENERGY = 0x0008 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - IMPORTED_ENERGY = 0x0001, - EXPORTED_ENERGY = 0x0002, - CUMULATIVE_ENERGY = 0x0004, - PERIODIC_ENERGY = 0x0008, -} - -Feature.is_imported_energy_set = function(self) - return (self.value & self.IMPORTED_ENERGY) ~= 0 -end - -Feature.set_imported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.IMPORTED_ENERGY - else - self.value = self.IMPORTED_ENERGY - end -end - -Feature.unset_imported_energy = function(self) - self.value = self.value & (~self.IMPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_exported_energy_set = function(self) - return (self.value & self.EXPORTED_ENERGY) ~= 0 -end - -Feature.set_exported_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.EXPORTED_ENERGY - else - self.value = self.EXPORTED_ENERGY - end -end - -Feature.unset_exported_energy = function(self) - self.value = self.value & (~self.EXPORTED_ENERGY & self.BASE_MASK) -end -Feature.is_cumulative_energy_set = function(self) - return (self.value & self.CUMULATIVE_ENERGY) ~= 0 -end - -Feature.set_cumulative_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.CUMULATIVE_ENERGY - else - self.value = self.CUMULATIVE_ENERGY - end -end - -Feature.unset_cumulative_energy = function(self) - self.value = self.value & (~self.CUMULATIVE_ENERGY & self.BASE_MASK) -end -Feature.is_periodic_energy_set = function(self) - return (self.value & self.PERIODIC_ENERGY) ~= 0 -end - -Feature.set_periodic_energy = function(self) - if self.value ~= nil then - self.value = self.value | self.PERIODIC_ENERGY - else - self.value = self.PERIODIC_ENERGY - end -end - -Feature.unset_periodic_energy = function(self) - self.value = self.value & (~self.PERIODIC_ENERGY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.IMPORTED_ENERGY | - Feature.EXPORTED_ENERGY | - Feature.CUMULATIVE_ENERGY | - Feature.PERIODIC_ENERGY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_imported_energy_set = Feature.is_imported_energy_set, - set_imported_energy = Feature.set_imported_energy, - unset_imported_energy = Feature.unset_imported_energy, - is_exported_energy_set = Feature.is_exported_energy_set, - set_exported_energy = Feature.set_exported_energy, - unset_exported_energy = Feature.unset_exported_energy, - is_cumulative_energy_set = Feature.is_cumulative_energy_set, - set_cumulative_energy = Feature.set_cumulative_energy, - unset_cumulative_energy = Feature.unset_cumulative_energy, - is_periodic_energy_set = Feature.is_periodic_energy_set, - set_periodic_energy = Feature.set_periodic_energy, - unset_periodic_energy = Feature.unset_periodic_energy, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua deleted file mode 100644 index 54785d16c6..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/init.lua +++ /dev/null @@ -1,94 +0,0 @@ -local cluster_base = require "st.matter.cluster_base" -local ElectricalPowerMeasurementServerAttributes = require "ElectricalPowerMeasurement.server.attributes" -local ElectricalPowerMeasurementTypes = require "ElectricalPowerMeasurement.types" - -local ElectricalPowerMeasurement = {} - -ElectricalPowerMeasurement.ID = 0x0090 -ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" -ElectricalPowerMeasurement.server = {} -ElectricalPowerMeasurement.client = {} -ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) -ElectricalPowerMeasurement.types = ElectricalPowerMeasurementTypes - -function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) - local attr_id_map = { - [0x0000] = "PowerMode", - [0x0001] = "NumberOfMeasurementTypes", - [0x0002] = "Accuracy", - [0x0003] = "Ranges", - [0x0004] = "Voltage", - [0x0005] = "ActiveCurrent", - [0x0006] = "ReactiveCurrent", - [0x0007] = "ApparentCurrent", - [0x0008] = "ActivePower", - [0x0009] = "ReactivePower", - [0x000A] = "ApparentPower", - [0x000B] = "RMSVoltage", - [0x000C] = "RMSCurrent", - [0x000D] = "RMSPower", - [0x000E] = "Frequency", - [0x000F] = "HarmonicCurrents", - [0x0010] = "HarmonicPhases", - [0x0011] = "PowerFactor", - [0x0012] = "NeutralCurrent", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", - } - local attr_name = attr_id_map[attr_id] - if attr_name ~= nil then - return self.attributes[attr_name] - end - return nil -end - -ElectricalPowerMeasurement.attribute_direction_map = { - ["PowerMode"] = "server", - ["NumberOfMeasurementTypes"] = "server", - ["Accuracy"] = "server", - ["Ranges"] = "server", - ["Voltage"] = "server", - ["ActiveCurrent"] = "server", - ["ReactiveCurrent"] = "server", - ["ApparentCurrent"] = "server", - ["ActivePower"] = "server", - ["ReactivePower"] = "server", - ["ApparentPower"] = "server", - ["RMSVoltage"] = "server", - ["RMSCurrent"] = "server", - ["RMSPower"] = "server", - ["Frequency"] = "server", - ["HarmonicCurrents"] = "server", - ["HarmonicPhases"] = "server", - ["PowerFactor"] = "server", - ["NeutralCurrent"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", -} - -ElectricalPowerMeasurement.FeatureMap = ElectricalPowerMeasurement.types.Feature - -function ElectricalPowerMeasurement.are_features_supported(feature, feature_map) - if (ElectricalPowerMeasurement.FeatureMap.bits_are_valid(feature)) then - return (feature & feature_map) == feature - end - return false -end - -local attribute_helper_mt = {} -attribute_helper_mt.__index = function(self, key) - local direction = ElectricalPowerMeasurement.attribute_direction_map[key] - if direction == nil then - error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) - end - return ElectricalPowerMeasurement[direction].attributes[key] -end -ElectricalPowerMeasurement.attributes = {} -setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) - -setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) - -return ElectricalPowerMeasurement - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua deleted file mode 100644 index cbda4f3478..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/Feature.lua +++ /dev/null @@ -1,138 +0,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.DIRECT_CURRENT = 0x0001 -Feature.ALTERNATING_CURRENT = 0x0002 -Feature.POLYPHASE_POWER = 0x0004 -Feature.HARMONICS = 0x0008 -Feature.POWER_QUALITY = 0x0010 - -Feature.mask_fields = { - BASE_MASK = 0xFFFF, - DIRECT_CURRENT = 0x0001, - ALTERNATING_CURRENT = 0x0002, - POLYPHASE_POWER = 0x0004, - HARMONICS = 0x0008, - POWER_QUALITY = 0x0010, -} - -Feature.is_direct_current_set = function(self) - return (self.value & self.DIRECT_CURRENT) ~= 0 -end - -Feature.set_direct_current = function(self) - if self.value ~= nil then - self.value = self.value | self.DIRECT_CURRENT - else - self.value = self.DIRECT_CURRENT - end -end - -Feature.unset_direct_current = function(self) - self.value = self.value & (~self.DIRECT_CURRENT & self.BASE_MASK) -end -Feature.is_alternating_current_set = function(self) - return (self.value & self.ALTERNATING_CURRENT) ~= 0 -end - -Feature.set_alternating_current = function(self) - if self.value ~= nil then - self.value = self.value | self.ALTERNATING_CURRENT - else - self.value = self.ALTERNATING_CURRENT - end -end - -Feature.unset_alternating_current = function(self) - self.value = self.value & (~self.ALTERNATING_CURRENT & self.BASE_MASK) -end -Feature.is_polyphase_power_set = function(self) - return (self.value & self.POLYPHASE_POWER) ~= 0 -end - -Feature.set_polyphase_power = function(self) - if self.value ~= nil then - self.value = self.value | self.POLYPHASE_POWER - else - self.value = self.POLYPHASE_POWER - end -end - -Feature.unset_polyphase_power = function(self) - self.value = self.value & (~self.POLYPHASE_POWER & self.BASE_MASK) -end -Feature.is_harmonics_set = function(self) - return (self.value & self.HARMONICS) ~= 0 -end - -Feature.set_harmonics = function(self) - if self.value ~= nil then - self.value = self.value | self.HARMONICS - else - self.value = self.HARMONICS - end -end - -Feature.unset_harmonics = function(self) - self.value = self.value & (~self.HARMONICS & self.BASE_MASK) -end -Feature.is_power_quality_set = function(self) - return (self.value & self.POWER_QUALITY) ~= 0 -end - -Feature.set_power_quality = function(self) - if self.value ~= nil then - self.value = self.value | self.POWER_QUALITY - else - self.value = self.POWER_QUALITY - end -end - -Feature.unset_power_quality = function(self) - self.value = self.value & (~self.POWER_QUALITY & self.BASE_MASK) -end - -function Feature.bits_are_valid(feature) - local max = - Feature.DIRECT_CURRENT | - Feature.ALTERNATING_CURRENT | - Feature.POLYPHASE_POWER | - Feature.HARMONICS | - Feature.POWER_QUALITY - if (feature <= max) and (feature >= 1) then - return true - else - return false - end -end - -Feature.mask_methods = { - is_direct_current_set = Feature.is_direct_current_set, - set_direct_current = Feature.set_direct_current, - unset_direct_current = Feature.unset_direct_current, - is_alternating_current_set = Feature.is_alternating_current_set, - set_alternating_current = Feature.set_alternating_current, - unset_alternating_current = Feature.unset_alternating_current, - is_polyphase_power_set = Feature.is_polyphase_power_set, - set_polyphase_power = Feature.set_polyphase_power, - unset_polyphase_power = Feature.unset_polyphase_power, - is_harmonics_set = Feature.is_harmonics_set, - set_harmonics = Feature.set_harmonics, - unset_harmonics = Feature.unset_harmonics, - is_power_quality_set = Feature.is_power_quality_set, - set_power_quality = Feature.set_power_quality, - unset_power_quality = Feature.unset_power_quality, -} - -Feature.augment_type = function(cls, val) - setmetatable(val, new_mt) -end - -setmetatable(Feature, new_mt) - -return Feature - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua deleted file mode 100644 index 16d13a0688..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/types/init.lua +++ /dev/null @@ -1,15 +0,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("ElectricalPowerMeasurement.types." .. key) - end - return types_mt.__types_cache[key] -end - -local ElectricalPowerMeasurementTypes = {} - -setmetatable(ElectricalPowerMeasurementTypes, types_mt) - -return ElectricalPowerMeasurementTypes - diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua deleted file mode 100644 index f2ca149f03..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/AttributeList.lua +++ /dev/null @@ -1,75 +0,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 AttributeList = { - ID = 0xFFFB, - NAME = "AttributeList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AttributeList: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, AttributeList.element_type) - end -end - -function AttributeList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AttributeList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil --event_id - ) -end - -function AttributeList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AttributeList: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 AttributeList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) -return AttributeList - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua deleted file mode 100644 index 50127a3537..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua +++ /dev/null @@ -1,41 +0,0 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" - -local LevelValue = { - ID = 0x000A, - NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", -} - -function LevelValue:new_value(...) - ConcentrationMeasurementServerAttributesLevelValue:new_value(...) -end - -function LevelValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function LevelValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function LevelValue:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function LevelValue:deserialize(tlv_buf) - return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) -end - -setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) -return LevelValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua deleted file mode 100644 index 763bbaa17b..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ /dev/null @@ -1,56 +0,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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" - - -local MeasuredValue = { - ID = 0x0000, - NAME = "MeasuredValue", - base_type = require "st.matter.data_types.SinglePrecisionFloat", -} - -function MeasuredValue:new_value(...) - return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) -end - -function MeasuredValue:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasuredValue:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) -return MeasuredValue - diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua deleted file mode 100644 index d18f14b09f..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ /dev/null @@ -1,45 +0,0 @@ -local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" - - -local MeasurementUnit = { - ID = 0x0008, - NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", -} - -function MeasurementUnit:new_value(...) - return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) -end - -function MeasurementUnit:read(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:subscribe(device, endpoint_id) - return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) -end - -function MeasurementUnit:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function MeasurementUnit:build_test_report_data( - device, - endpoint_id, - value, - status -) - return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) -end - -function MeasurementUnit:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - self:augment_type(data) - return data -end - -setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) -return MeasurementUnit - diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua deleted file mode 100644 index 6a8d95df1d..0000000000 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/AcceptedCommandList.lua +++ /dev/null @@ -1,75 +0,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 AcceptedCommandList = { - ID = 0xFFF9, - NAME = "AcceptedCommandList", - base_type = require "st.matter.data_types.Array", - element_type = require "st.matter.data_types.Uint32", -} - -function AcceptedCommandList: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, AcceptedCommandList.element_type) - end -end - -function AcceptedCommandList:new_value(...) - local o = self.base_type(table.unpack({...})) - - return o -end - -function AcceptedCommandList:read(device, endpoint_id) - return cluster_base.read( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:subscribe(device, endpoint_id) - return cluster_base.subscribe( - device, - endpoint_id, - self._cluster.ID, - self.ID, - nil - ) -end - -function AcceptedCommandList:set_parent_cluster(cluster) - self._cluster = cluster - return self -end - -function AcceptedCommandList: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 AcceptedCommandList:deserialize(tlv_buf) - local data = TLVParser.decode_tlv(tlv_buf) - - return data -end - -setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) -return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua index 3fa5f37100..7bfdfd7248 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" -local ActivatedCarbonFilterMonitoringServerCommands = require "ActivatedCarbonFilterMonitoring.server.commands" -local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" +local ActivatedCarbonFilterMonitoringServerAttributes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes" +local ActivatedCarbonFilterMonitoringServerCommands = require "embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands" +local ActivatedCarbonFilterMonitoringTypes = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types" local ActivatedCarbonFilterMonitoring = {} diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua index 4980356485..df65143e66 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local ChangeIndication = { ID = 0x0002, NAME = "ChangeIndication", - base_type = require "ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", + base_type = require "embedded_clusters.ActivatedCarbonFilterMonitoring.types.ChangeIndicationEnum", } function ChangeIndication:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua index e668aa4c48..361e423a1c 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/Condition.lua @@ -1,4 +1,7 @@ -local cluster_base = require "st.matter.cluster_base" +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua index a02378a50d..1ecdf6a931 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ActivatedCarbonFilterMonitoring.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua index 040ce653b4..1ac942f780 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/ResetCondition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua index 7f641481d4..ec343da578 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("ActivatedCarbonFilterMonitoring.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.ActivatedCarbonFilterMonitoring.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua index 438de24c94..6b5ab62494 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/ChangeIndicationEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua index 88474d1b0f..906e769676 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua index 2ff8e6e89a..7cdc2d1561 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ActivatedCarbonFilterMonitoring/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ActivatedCarbonFilterMonitoring.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ActivatedCarbonFilterMonitoring.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua similarity index 86% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua index 78e77fce51..4f6d19cc9f 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local AirQualityServerAttributes = require "AirQuality.server.attributes" -local AirQualityTypes = require "AirQuality.types" +local AirQualityServerAttributes = require "embedded_clusters.AirQuality.server.attributes" +local AirQualityTypes = require "embedded_clusters.AirQuality.types" local AirQuality = {} diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua index 6a8d95df1d..a1e56c6597 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AcceptedCommandList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AcceptedCommandList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua index f92f43543a..1beb6218f9 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AirQuality.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AirQuality.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local AirQuality = { ID = 0x0000, NAME = "AirQuality", - base_type = require "AirQuality.types.AirQualityEnum", + base_type = require "embedded_clusters.AirQuality.types.AirQualityEnum", } function AirQuality:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua index 93e96817e6..238b50ade3 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/AttributeList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua index 69155cd7ca..719f17a231 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/EventList.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/EventList.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua index aef3b476a9..50295b081a 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("AirQuality.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.AirQuality.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua index 317a42dc9b..c2c255614a 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/AirQualityEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/AirQualityEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua index 86b90ce627..906a09a2bb 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua similarity index 60% rename from drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua index 88a2b861b7..b77d67de82 100644 --- a/drivers/SmartThings/matter-thermostat/src/AirQuality/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/AirQuality/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("AirQuality.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.AirQuality.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua index f5109f8943..4de97147e4 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonDioxideConcentrationMeasurementServerAttributes = require "CarbonDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua index 93583c2080..0206213e6f 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua index e8cdb487f5..a6e1f24d1d 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local CarbonMonoxideConcentrationMeasurementServerAttributes = require "CarbonMonoxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local CarbonMonoxideConcentrationMeasurementServerAttributes = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local CarbonMonoxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua index 2307e8977d..1a7e7b508c 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/CarbonMonoxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.CarbonMonoxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua index 596d1bfe80..cb4cffa2d2 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ConcentrationMeasurementServerAttributes = require "ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurementTypes = require "ConcentrationMeasurement.types" +local ConcentrationMeasurementServerAttributes = require "embedded_clusters.ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurementTypes = require "embedded_clusters.ConcentrationMeasurement.types" local ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua index e7023a336c..e4f88c8491 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua index c658d2d3aa..2ab739841f 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua index 3d50e8b97b..0fa14745c0 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua index a1a0092151..19cde9aa55 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua similarity index 98% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua index 9aa2413903..0bb19bec62 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua index b1264d72b8..02b4f727df 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/LevelValueEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/LevelValueEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua index c8302c5cc4..6efd90901a 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/MeasurementUnitEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/MeasurementUnitEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua index f6da1e6b62..c339f17414 100644 --- a/drivers/SmartThings/matter-thermostat/src/ConcentrationMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ConcentrationMeasurement/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ConcentrationMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ConcentrationMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua index bc5685ae75..0f564673a9 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/init.lua @@ -1,7 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local ElectricalEnergyMeasurementServerAttributes = require "ElectricalEnergyMeasurement.server.attributes" -local ElectricalEnergyMeasurementEvents = require "ElectricalEnergyMeasurement.server.events" -local ElectricalEnergyMeasurementTypes = require "ElectricalEnergyMeasurement.types" +local ElectricalEnergyMeasurementServerAttributes = require "embedded_clusters.ElectricalEnergyMeasurement.server.attributes" +local ElectricalEnergyMeasurementTypes = require "embedded_clusters.ElectricalEnergyMeasurement.types" local ElectricalEnergyMeasurement = {} ElectricalEnergyMeasurement.ID = 0x0091 @@ -9,20 +11,12 @@ ElectricalEnergyMeasurement.NAME = "ElectricalEnergyMeasurement" ElectricalEnergyMeasurement.server = {} ElectricalEnergyMeasurement.client = {} ElectricalEnergyMeasurement.server.attributes = ElectricalEnergyMeasurementServerAttributes:set_parent_cluster(ElectricalEnergyMeasurement) -ElectricalEnergyMeasurement.server.events = ElectricalEnergyMeasurementEvents:set_parent_cluster(ElectricalEnergyMeasurement) ElectricalEnergyMeasurement.types = ElectricalEnergyMeasurementTypes function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) local attr_id_map = { - [0x0000] = "Accuracy", [0x0001] = "CumulativeEnergyImported", - [0x0002] = "CumulativeEnergyExported", [0x0003] = "PeriodicEnergyImported", - [0x0004] = "PeriodicEnergyExported", - [0x0005] = "CumulativeEnergyReset", - [0xFFF9] = "AcceptedCommandList", - [0xFFFA] = "EventList", - [0xFFFB] = "AttributeList", } local attr_name = attr_id_map[attr_id] if attr_name ~= nil then @@ -32,15 +26,8 @@ function ElectricalEnergyMeasurement:get_attribute_by_id(attr_id) end ElectricalEnergyMeasurement.attribute_direction_map = { - ["Accuracy"] = "server", ["CumulativeEnergyImported"] = "server", - ["CumulativeEnergyExported"] = "server", ["PeriodicEnergyImported"] = "server", - ["PeriodicEnergyExported"] = "server", - ["CumulativeEnergyReset"] = "server", - ["AcceptedCommandList"] = "server", - ["EventList"] = "server", - ["AttributeList"] = "server", } ElectricalEnergyMeasurement.FeatureMap = ElectricalEnergyMeasurement.types.Feature @@ -63,13 +50,6 @@ end ElectricalEnergyMeasurement.attributes = {} setmetatable(ElectricalEnergyMeasurement.attributes, attribute_helper_mt) -local event_helper_mt = {} -event_helper_mt.__index = function(self, key) - return ElectricalEnergyMeasurement.server.events[key] -end -ElectricalEnergyMeasurement.events = {} -setmetatable(ElectricalEnergyMeasurement.events, event_helper_mt) - setmetatable(ElectricalEnergyMeasurement, {__index = cluster_base}) return ElectricalEnergyMeasurement diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua index 3dc58635e1..2d41790440 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/CumulativeEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local CumulativeEnergyImported = { ID = 0x0001, NAME = "CumulativeEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function CumulativeEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua index 753b91ea2d..5daccf48ab 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/PeriodicEnergyImported.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local PeriodicEnergyImported = { ID = 0x0003, NAME = "PeriodicEnergyImported", - base_type = require "ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", + base_type = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct", } function PeriodicEnergyImported:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua index adfdf42bbf..57bc0d1f72 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ElectricalEnergyMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalEnergyMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua similarity index 97% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua index 950b260227..a4c58a3646 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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 EnergyMeasurementStruct = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua new file mode 100644 index 0000000000..e3db76f49d --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/Feature.lua @@ -0,0 +1,31 @@ +-- Copyright © 2025 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.IMPORTED_ENERGY = 0x0001 +Feature.EXPORTED_ENERGY = 0x0002 +Feature.CUMULATIVE_ENERGY = 0x0004 +Feature.PERIODIC_ENERGY = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.IMPORTED_ENERGY | + Feature.EXPORTED_ENERGY | + Feature.CUMULATIVE_ENERGY | + Feature.PERIODIC_ENERGY + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua similarity index 62% rename from drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua index bb0c39fe0e..ec29b53e05 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalEnergyMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalEnergyMeasurement/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ElectricalEnergyMeasurement.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.ElectricalEnergyMeasurement.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua new file mode 100644 index 0000000000..c9061e3cc4 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/init.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local ElectricalPowerMeasurementServerAttributes = require "embedded_clusters.ElectricalPowerMeasurement.server.attributes" + +local ElectricalPowerMeasurement = {} + +ElectricalPowerMeasurement.ID = 0x0090 +ElectricalPowerMeasurement.NAME = "ElectricalPowerMeasurement" +ElectricalPowerMeasurement.server = {} +ElectricalPowerMeasurement.client = {} +ElectricalPowerMeasurement.server.attributes = ElectricalPowerMeasurementServerAttributes:set_parent_cluster(ElectricalPowerMeasurement) + +function ElectricalPowerMeasurement:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0008] = "ActivePower", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +ElectricalPowerMeasurement.attribute_direction_map = { + ["ActivePower"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = ElectricalPowerMeasurement.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, ElectricalPowerMeasurement.NAME)) + end + return ElectricalPowerMeasurement[direction].attributes[key] +end +ElectricalPowerMeasurement.attributes = {} +setmetatable(ElectricalPowerMeasurement.attributes, attribute_helper_mt) + +setmetatable(ElectricalPowerMeasurement, {__index = cluster_base}) + +return ElectricalPowerMeasurement + diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua index 6c34abd2f4..f1696509f5 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/ActivePower.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/ActivePower.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -65,4 +68,3 @@ end setmetatable(ActivePower, {__call = ActivePower.new_value, __index = ActivePower.base_type}) return ActivePower - diff --git a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua index 0c30fa8dd4..6de69b94ff 100644 --- a/drivers/SmartThings/matter-thermostat/src/ElectricalPowerMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/ElectricalPowerMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("ElectricalPowerMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.ElectricalPowerMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua index 5920a9dc66..cdfd1d597e 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local FormaldehydeConcentrationMeasurementServerAttributes = require "FormaldehydeConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local FormaldehydeConcentrationMeasurementServerAttributes = require "embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local FormaldehydeConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/CarbonDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua index 37900b0fb1..3dee13abe3 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/FormaldehydeConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("FormaldehydeConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.FormaldehydeConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua index 84400d7833..7a36d3aa92 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" -local HepaFilterMonitoringServerCommands = require "HepaFilterMonitoring.server.commands" -local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" +local HepaFilterMonitoringServerAttributes = require "embedded_clusters.HepaFilterMonitoring.server.attributes" +local HepaFilterMonitoringServerCommands = require "embedded_clusters.HepaFilterMonitoring.server.commands" +local HepaFilterMonitoringTypes = require "embedded_clusters.HepaFilterMonitoring.types" local HepaFilterMonitoring = {} diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua index 955b89eb88..9dca020204 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -5,7 +8,7 @@ local TLVParser = require "st.matter.TLV.TLVParser" local ChangeIndication = { ID = 0x0002, NAME = "ChangeIndication", - base_type = require "HepaFilterMonitoring.types.ChangeIndicationEnum", + base_type = require "embedded_clusters.HepaFilterMonitoring.types.ChangeIndicationEnum", } function ChangeIndication:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua index e668aa4c48..76a4fd9760 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/Condition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/Condition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua index 8d7ffe6c00..2590980846 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("HepaFilterMonitoring.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua index 040ce653b4..1ac942f780 100644 --- a/drivers/SmartThings/matter-thermostat/src/ActivatedCarbonFilterMonitoring/server/commands/ResetCondition.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/ResetCondition.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua index 55a4ea7a30..77ae141f77 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("HepaFilterMonitoring.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.HepaFilterMonitoring.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua index 438de24c94..6b5ab62494 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/ChangeIndicationEnum.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/ChangeIndicationEnum.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua index 88474d1b0f..906e769676 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua index 77aca088ff..6c36af1aec 100644 --- a/drivers/SmartThings/matter-thermostat/src/HepaFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/HepaFilterMonitoring/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("HepaFilterMonitoring.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.HepaFilterMonitoring.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua similarity index 88% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua index b60e71050a..eae9be65f0 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local NitrogenDioxideConcentrationMeasurementServerAttributes = require "NitrogenDioxideConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local NitrogenDioxideConcentrationMeasurementServerAttributes = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local NitrogenDioxideConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua similarity index 81% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua index 50127a3537..cc8f7b47eb 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/LevelValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/LevelValue.lua @@ -1,9 +1,12 @@ -local ConcentrationMeasurementServerAttributesLevelValue = require "ConcentrationMeasurement.server.attributes.LevelValue" +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" local LevelValue = { ID = 0x000A, NAME = "LevelValue", - base_type = require "ConcentrationMeasurement.types.LevelValueEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", } function LevelValue:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua index 763bbaa17b..ca0dafb0dc 100644 --- a/drivers/SmartThings/matter-thermostat/src/FormaldehydeConcentrationMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "ConcentrationMeasurement.server.attributes.MeasuredValue" +local ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" local MeasuredValue = { diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua similarity index 82% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua index d18f14b09f..9597906d3a 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -1,11 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local TLVParser = require "st.matter.TLV.TLVParser" -local ConcentrationMeasurementServerAttributesMeasurementUnit = require "ConcentrationMeasurement.server.attributes.MeasurementUnit" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" local MeasurementUnit = { ID = 0x0008, NAME = "MeasurementUnit", - base_type = require "ConcentrationMeasurement.types.MeasurementUnitEnum", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", } function MeasurementUnit:new_value(...) diff --git a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua index 06c3d6dd55..c82517d362 100644 --- a/drivers/SmartThings/matter-thermostat/src/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/NitrogenDioxideConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.NitrogenDioxideConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua index 83fd04857e..c49ea94b39 100644 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local OzoneConcentrationMeasurementServerAttributes = require "OzoneConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local OzoneConcentrationMeasurementServerAttributes = require "embedded_clusters.OzoneConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local OzoneConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua index fe0048cd99..918b680495 100644 --- a/drivers/SmartThings/matter-thermostat/src/OzoneConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/OzoneConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("OzoneConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.OzoneConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua index 98eebd407e..3b333b5417 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm10ConcentrationMeasurementServerAttributes = require "Pm10ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm10ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm10ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm10ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua index 55f08c7e43..3b1e6617a4 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm10ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm10ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm10ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm10ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua index 0b3caa3bd2..b2e6656a9a 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm1ConcentrationMeasurementServerAttributes = require "Pm1ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm1ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm1ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm1ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua index f668e41a07..2635da32a6 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm1ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm1ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm1ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm1ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua index 5234346d60..e6e6144f94 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local Pm25ConcentrationMeasurementServerAttributes = require "Pm25ConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local Pm25ConcentrationMeasurementServerAttributes = require "embedded_clusters.Pm25ConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local Pm25ConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua index 2c7d5fce7b..5c432da0ec 100644 --- a/drivers/SmartThings/matter-thermostat/src/Pm25ConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/Pm25ConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("Pm25ConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.Pm25ConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua similarity index 85% rename from drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua index 2a4cc04a0d..3d4b21d602 100644 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local RadonConcentrationMeasurementServerAttributes = require "RadonConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local RadonConcentrationMeasurementServerAttributes = require "embedded_clusters.RadonConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local RadonConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua index b83ef67bfc..8aa225e510 100644 --- a/drivers/SmartThings/matter-thermostat/src/RadonConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/RadonConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("RadonConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.RadonConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua similarity index 89% rename from drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua index a99c1dea50..cad49a14e1 100644 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/init.lua @@ -1,6 +1,9 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" -local ConcentrationMeasurement = require "ConcentrationMeasurement" +local TotalVolatileOrganicCompoundsConcentrationMeasurementServerAttributes = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes" +local ConcentrationMeasurement = require "embedded_clusters.ConcentrationMeasurement" local TotalVolatileOrganicCompoundsConcentrationMeasurement = {} diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua new file mode 100644 index 0000000000..cc8f7b47eb --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/LevelValue.lua @@ -0,0 +1,44 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ConcentrationMeasurementServerAttributesLevelValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.LevelValue" + +local LevelValue = { + ID = 0x000A, + NAME = "LevelValue", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.LevelValueEnum", +} + +function LevelValue:new_value(...) + ConcentrationMeasurementServerAttributesLevelValue:new_value(...) +end + +function LevelValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:read(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesLevelValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function LevelValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function LevelValue:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesLevelValue:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function LevelValue:deserialize(tlv_buf) + return ConcentrationMeasurementServerAttributesLevelValue:deserialize(tlv_buf) +end + +setmetatable(LevelValue, {__call = LevelValue.new_value, __index = LevelValue.base_type}) +return LevelValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua new file mode 100644 index 0000000000..ca0dafb0dc --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasuredValue.lua @@ -0,0 +1,59 @@ +-- Copyright © 2025 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 ConcentrationMeasurementServerAttributesMeasuredValue = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasuredValue" + + +local MeasuredValue = { + ID = 0x0000, + NAME = "MeasuredValue", + base_type = require "st.matter.data_types.SinglePrecisionFloat", +} + +function MeasuredValue:new_value(...) + return ConcentrationMeasurementServerAttributesMeasuredValue:new_value(...) +end + +function MeasuredValue:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:read(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasuredValue:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasuredValue:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasuredValue: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 MeasuredValue:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) +return MeasuredValue + diff --git a/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua new file mode 100644 index 0000000000..9597906d3a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/MeasurementUnit.lua @@ -0,0 +1,48 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local TLVParser = require "st.matter.TLV.TLVParser" +local ConcentrationMeasurementServerAttributesMeasurementUnit = require "embedded_clusters.ConcentrationMeasurement.server.attributes.MeasurementUnit" + + +local MeasurementUnit = { + ID = 0x0008, + NAME = "MeasurementUnit", + base_type = require "embedded_clusters.ConcentrationMeasurement.types.MeasurementUnitEnum", +} + +function MeasurementUnit:new_value(...) + return ConcentrationMeasurementServerAttributesMeasurementUnit:new_value(...) +end + +function MeasurementUnit:read(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:read(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:subscribe(device, endpoint_id) + return ConcentrationMeasurementServerAttributesMeasurementUnit:subscribe(device, endpoint_id, self._cluster.ID) +end + +function MeasurementUnit:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function MeasurementUnit:build_test_report_data( + device, + endpoint_id, + value, + status +) + return ConcentrationMeasurementServerAttributesMeasurementUnit:build_test_report_data(device, endpoint_id, value, status, self._cluster.ID) +end + +function MeasurementUnit:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(MeasurementUnit, {__call = MeasurementUnit.new_value, __index = MeasurementUnit.base_type}) +return MeasurementUnit + diff --git a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua index c0c1b65c37..b67cbcd7b6 100644 --- a/drivers/SmartThings/matter-thermostat/src/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/TotalVolatileOrganicCompoundsConcentrationMeasurement/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua similarity index 87% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua index 1155cfd636..3a1c2f1bfb 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/init.lua @@ -1,7 +1,10 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cluster_base = require "st.matter.cluster_base" -local WaterHeaterModeServerAttributes = require "WaterHeaterMode.server.attributes" -local WaterHeaterModeServerCommands = require "WaterHeaterMode.server.commands" -local WaterHeaterModeTypes = require "WaterHeaterMode.types" +local WaterHeaterModeServerAttributes = require "embedded_clusters.WaterHeaterMode.server.attributes" +local WaterHeaterModeServerCommands = require "embedded_clusters.WaterHeaterMode.server.commands" +local WaterHeaterModeTypes = require "embedded_clusters.WaterHeaterMode.types" local WaterHeaterMode = {} diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua similarity index 93% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua index aa20156f74..165b906df5 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/CurrentMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/CurrentMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -24,7 +27,6 @@ function CurrentMode:read(device, endpoint_id) ) end - function CurrentMode:subscribe(device, endpoint_id) return cluster_base.subscribe( device, diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua index 1f393a17d4..903a374d43 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/SupportedModes.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/SupportedModes.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -6,7 +9,7 @@ local SupportedModes = { ID = 0x0000, NAME = "SupportedModes", base_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeOptionStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeOptionStruct", } function SupportedModes:augment_type(data_type_obj) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua similarity index 75% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua index 020a4125ce..fb7bed9828 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/attributes/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/attributes/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("WaterHeaterMode.server.attributes.%s", key) + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.attributes.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua index 73ddbd1029..726352b448 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/ChangeToMode.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/ChangeToMode.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.matter.data_types" local TLVParser = require "st.matter.TLV.TLVParser" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua similarity index 76% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua index 9736c4577f..64660ed336 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/server/commands/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/server/commands/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local command_mt = {} command_mt.__command_cache = {} command_mt.__index = function(self, key) if command_mt.__command_cache[key] == nil then - local req_loc = string.format("WaterHeaterMode.server.commands.%s", key) + local req_loc = string.format("embedded_clusters.WaterHeaterMode.server.commands.%s", key) local raw_def = require(req_loc) local cluster = rawget(self, "_cluster") command_mt.__command_cache[key] = raw_def:set_parent_cluster(cluster) diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua similarity index 92% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua index da49bf4115..c7e987399d 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/Feature.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/Feature.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua similarity index 94% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua index f770a7916c..1db8c5b634 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeOptionStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeOptionStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" @@ -24,7 +27,7 @@ ModeOptionStruct.field_defs = { is_nullable = false, is_optional = false, data_type = require "st.matter.data_types.Array", - element_type = require "WaterHeaterMode.types.ModeTagStruct", + element_type = require "embedded_clusters.WaterHeaterMode.types.ModeTagStruct", }, } diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua similarity index 90% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua index 009e70a40e..7d37d4374d 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTag.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTag.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua similarity index 96% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua index 1c41eb320e..17dfd269d6 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/ModeTagStruct.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/ModeTagStruct.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 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" diff --git a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua similarity index 61% rename from drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua rename to drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua index f0198ff8a0..bae583dfb4 100644 --- a/drivers/SmartThings/matter-thermostat/src/WaterHeaterMode/types/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/embedded_clusters/WaterHeaterMode/types/init.lua @@ -1,8 +1,11 @@ +-- Copyright © 2025 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("WaterHeaterMode.types." .. key) + types_mt.__types_cache[key] = require("embedded_clusters.WaterHeaterMode.types." .. key) end return types_mt.__types_cache[key] end diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 58f0d75344..6697c80fb6 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -1,2263 +1,516 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local log = require "log" -local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "embedded-cluster-utils" -local im = require "st.matter.interaction_model" - -local MatterDriver = require "st.matter.driver" -local utils = require "st.utils" - -local SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" --- declare match_profile function for use throughout file -local match_profile - --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" -if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" - -- new modes add in Matter 1.2 - clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 - clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -end - -local SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" -local DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" -local OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" - -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" -end - -if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" -end - -local THERMOSTAT_MODE_MAP = { - [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, - [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, - [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, - [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, - [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, - [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, - [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, - [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, - [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, -} - -local THERMOSTAT_OPERATING_MODE_MAP = { - [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, - [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, - [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, - [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, -} - -local WIND_MODE_MAP = { - [0] = capabilities.windMode.windMode.sleepWind, - [1] = capabilities.windMode.windMode.naturalWind -} - -local ROCK_MODE_MAP = { - [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, - [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, - [2] = capabilities.fanOscillationMode.fanOscillationMode.swing -} - -local RAC_DEVICE_TYPE_ID = 0x0072 -local AP_DEVICE_TYPE_ID = 0x002D -local FAN_DEVICE_TYPE_ID = 0x002B -local WATER_HEATER_DEVICE_TYPE_ID = 0x050F -local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 -local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 -local ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 - -local MIN_ALLOWED_PERCENT_VALUE = 0 -local MAX_ALLOWED_PERCENT_VALUE = 100 - -local CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -local LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" -local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds - -local TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" -local SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" -local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -local MGM3_PPM_CONVERSION_FACTOR = 24.45 - --- For RPC version >= 6, we can always assume that the values received from temperatureSetpoint --- are in Celsius, but we still limit the setpoint range to somewhat reasonable values. --- For RPC <= 5, this is a work around to handle when units for temperatureSetpoint is changed for the App. --- When units are switched, we will never know the units of the received command value as the arguments don't contain the unit. --- So to handle this we assume the following ranges considering usual thermostat/water-heater temperatures: --- Thermostat: --- 1. if the received setpoint command value is in range 5 ~ 40, it is inferred as *C --- 2. if the received setpoint command value is in range 41 ~ 104, it is inferred as *F -local THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 -local THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 --- Water Heater: --- 1. if the received setpoint command value is in range 30 ~ 80, it is inferred as *C --- 2. if the received setpoint command value is in range 86 ~ 176, it is inferred as *F -local WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 -local WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 - -local setpoint_limit_device_field = { - MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", - MIN_HEAT = "MIN_HEAT", - MAX_HEAT = "MAX_HEAT", - MIN_COOL = "MIN_COOL", - MAX_COOL = "MAX_COOL", - MIN_DEADBAND = "MIN_DEADBAND", - MIN_TEMP = "MIN_TEMP", - MAX_TEMP = "MAX_TEMP" -} - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local profiling_data = { - BATTERY_SUPPORT = "__BATTERY_SUPPORT", - THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" -} - -local subscribed_attributes = { - [capabilities.switch.ID] = { - clusters.OnOff.attributes.OnOff - }, - [capabilities.temperatureMeasurement.ID] = { - clusters.Thermostat.attributes.LocalTemperature, - clusters.TemperatureMeasurement.attributes.MeasuredValue, - clusters.TemperatureMeasurement.attributes.MinMeasuredValue, - clusters.TemperatureMeasurement.attributes.MaxMeasuredValue - }, - [capabilities.relativeHumidityMeasurement.ID] = { - clusters.RelativeHumidityMeasurement.attributes.MeasuredValue - }, - [capabilities.thermostatMode.ID] = { - clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ControlSequenceOfOperation - }, - [capabilities.thermostatOperatingState.ID] = { - clusters.Thermostat.attributes.ThermostatRunningState - }, - [capabilities.thermostatFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.thermostatCoolingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedCoolingSetpoint, - clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, - clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit - }, - [capabilities.thermostatHeatingSetpoint.ID] = { - clusters.Thermostat.attributes.OccupiedHeatingSetpoint, - clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, - clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit - }, - [capabilities.airConditionerFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.airPurifierFanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanMode.ID] = { - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode - }, - [capabilities.fanSpeedPercent.ID] = { - clusters.FanControl.attributes.PercentCurrent - }, - [capabilities.windMode.ID] = { - clusters.FanControl.attributes.WindSupport, - clusters.FanControl.attributes.WindSetting - }, - [capabilities.fanOscillationMode.ID] = { - clusters.FanControl.attributes.RockSupport, - clusters.FanControl.attributes.RockSetting - }, - [capabilities.battery.ID] = { - clusters.PowerSource.attributes.BatPercentRemaining - }, - [capabilities.batteryLevel.ID] = { - clusters.PowerSource.attributes.BatChargeLevel - }, - [capabilities.filterState.ID] = { - clusters.HepaFilterMonitoring.attributes.Condition, - clusters.ActivatedCarbonFilterMonitoring.attributes.Condition - }, - [capabilities.filterStatus.ID] = { - clusters.HepaFilterMonitoring.attributes.ChangeIndication, - clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication - }, - [capabilities.airQualityHealthConcern.ID] = { - clusters.AirQuality.attributes.AirQuality - }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.nitrogenDioxideMeasurement.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.nitrogenDioxideHealthConcern.ID] = { - clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.ozoneMeasurement.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, - clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit - }, - [capabilities.ozoneHealthConcern.ID] = { - clusters.OzoneConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.formaldehydeMeasurement.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, - clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.formaldehydeHealthConcern.ID] = { - clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.veryFineDustSensor.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.veryFineDustHealthConcern.ID] = { - clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustHealthConcern.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.fineDustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustSensor.ID] = { - clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, - clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, - clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.dustHealthConcern.ID] = { - clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.radonMeasurement.ID] = { - clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, - clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.radonHealthConcern.ID] = { - clusters.RadonConcentrationMeasurement.attributes.LevelValue, - }, - [capabilities.tvocMeasurement.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.tvocHealthConcern.ID] = { - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue - }, - [capabilities.powerMeter.ID] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower - }, - [capabilities.mode.ID] = { - clusters.WaterHeaterMode.attributes.CurrentMode, - clusters.WaterHeaterMode.attributes.SupportedModes - }, - [capabilities.powerConsumptionReport.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported - }, - [capabilities.energyMeter.ID] = { - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported - }, -} - -local function supports_capability_by_id_modular(device, capability, component) - if not device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - -local function epoch_to_iso8601(time) - return os.date("!%Y-%m-%dT%H:%M:%SZ", time) -end - -local get_total_cumulative_energy_imported = function(device) - local total_cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local total_energy = 0 - for _, energyWh in pairs(total_cumulative_energy_imported) do - total_energy = total_energy + energyWh - end - return total_energy -end - -local function report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) - local current_time = os.time() - local last_time = device:get_field(LAST_IMPORTED_REPORT_TIMESTAMP) or 0 - - -- Ensure that the previous report was sent at least 15 minutes ago - if MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then - return - end - - device:set_field(LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) -end - -local function device_removed(driver, device) - device.log.info("device removed") -end - -local function tbl_contains(array, value) - for idx, element in ipairs(array) do - if element == value then - return true, idx - end - end - return false, nil -end - -local function get_field_for_endpoint(device, field, endpoint) - return device:get_field(string.format("%s_%d", field, endpoint)) -end - -local function set_field_for_endpoint(device, field, endpoint, value, additional_params) - device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) -end - -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - 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 - 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, cluster_id) - -- Use the find_default_endpoint function to return the first endpoint that - -- supports a given cluster. - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then - return component_to_endpoint_map[component_name] - end - if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end - return find_default_endpoint(device, cluster_id) -end - -local endpoint_to_component = function (device, endpoint_id) - local component_to_endpoint_map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) - if component_to_endpoint_map ~= nil then - for comp, ep in pairs(component_to_endpoint_map) do - if ep == endpoint_id then - return comp - end - end - end - return "main" -end - -local function device_init(driver, device) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then - -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id - -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - device:subscribe() - device:set_component_to_endpoint_fn(component_to_endpoint) - device:set_endpoint_to_component_fn(endpoint_to_component) - if not device:get_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then - local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - --Query min setpoint deadband if needed - if #auto_eps ~= 0 and device:get_field(setpoint_limit_device_field.MIN_DEADBAND) == nil then - local deadband_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - deadband_read:merge(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) - device:send(deadband_read) - end - end - - -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. - -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. - local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - if #electrical_energy_measurement_eps > 0 then - local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( - device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} - ) - if #cumulative_energy_eps == 0 then device:set_field(CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end - end -end - -local function info_changed(driver, device, event, args) - if device:get_field(SUPPORTED_COMPONENT_CAPABILITIES) then - -- This indicates the device should be using a modular profile, so - -- re-up subscription with new capabilities using the modular supports_capability override - device:extend_device("supports_capability_by_id", supports_capability_by_id_modular) - end - - if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - end -end - -local function get_endpoints_for_dt(device, device_type) - local endpoints = {} - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == device_type then - table.insert(endpoints, ep.endpoint_id) - break - end - end - end - table.sort(endpoints) - return endpoints -end - -local function get_device_type(device) - -- For cases where a device has multiple device types, this list indicates which - -- device type will be the "main" device type for purposes of selecting a profile - -- with an appropriate category. This is done to promote consistency between - -- devices with similar device type compositions that may report their device types - -- listed in different orders - local device_type_priority = { - [HEAT_PUMP_DEVICE_TYPE_ID] = 1, - [RAC_DEVICE_TYPE_ID] = 2, - [AP_DEVICE_TYPE_ID] = 3, - [THERMOSTAT_DEVICE_TYPE_ID] = 4, - [FAN_DEVICE_TYPE_ID] = 5, - [WATER_HEATER_DEVICE_TYPE_ID] = 6, - } - - local main_device_type = false - - for _, ep in ipairs(device.endpoints) do - if ep.device_types ~= nil then - for _, dt in ipairs(ep.device_types) do - if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and - device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then - main_device_type = dt.device_type_id - end - end - end - end - - return main_device_type -end - -local AIR_QUALITY_MAP = { - {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, - {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, - {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, - {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, - {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, - {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, - {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, - {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, - {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, - {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, - {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, -} - -local function create_level_measurement_profile(device) - local meas_name, level_name = "", "" - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - level_name = level_name .. details[2] - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - meas_name = meas_name .. details[2] - end - end - end - return meas_name, level_name -end - -local function supported_level_measurements(device) - local measurement_caps, level_caps = {}, {} - for _, details in ipairs(AIR_QUALITY_MAP) do - local cap_id = details[1] - local cluster = details[3] - -- capability describes either a HealthConcern or Measurement/Sensor - if (cap_id:match("HealthConcern$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) - if #attr_eps > 0 then - table.insert(level_caps, cap_id) - end - elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then - local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) - if #attr_eps > 0 then - table.insert(measurement_caps, cap_id) - end - end - end - return measurement_caps, level_caps -end - -local function create_air_quality_sensor_profile(device) - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - local profile_name = "" - if #aqs_eps > 0 then - profile_name = profile_name .. "-aqs" - end - local meas_name, level_name = create_level_measurement_profile(device) - if meas_name ~= "" then - profile_name = profile_name .. meas_name .. "-meas" - end - if level_name ~= "" then - profile_name = profile_name .. level_name .. "-level" - end - return profile_name -end - -local function create_fan_profile(device) - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local profile_name = "" - if #fan_eps > 0 then - profile_name = profile_name .. "-fan" - end - if #rock_eps > 0 then - profile_name = profile_name .. "-rock" - end - if #wind_eps > 0 then - profile_name = profile_name .. "-wind" - end - return profile_name -end - -local function create_air_purifier_profile(device) - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - local fan_eps_seen = false - local profile_name = "air-purifier" - if #hepa_filter_eps > 0 then - profile_name = profile_name .. "-hepa" - end - if #ac_filter_eps > 0 then - profile_name = profile_name .. "-ac" - end - - -- air purifier profiles include -fan later in the name for historical reasons. - -- save this information for use at that point. - local fan_profile = create_fan_profile(device) - if fan_profile ~= "" then - fan_eps_seen = true - end - fan_profile = string.gsub(fan_profile, "-fan", "") - profile_name = profile_name .. fan_profile - - return profile_name, fan_eps_seen -end - -local function create_thermostat_modes_profile(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - local thermostat_modes = "" - if #heat_eps == 0 and #cool_eps == 0 then - return "No Heating nor Cooling Support" - elseif #heat_eps > 0 and #cool_eps == 0 then - thermostat_modes = thermostat_modes .. "-heating-only" - elseif #cool_eps > 0 and #heat_eps == 0 then - thermostat_modes = thermostat_modes .. "-cooling-only" - end - return thermostat_modes -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_switch(driver, device) - if profiling_data_still_required(device) then return end - - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local device_type = get_device_type(device) - local profile_name - if device_type == RAC_DEVICE_TYPE_ID then - profile_name = "room-air-conditioner" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- Room AC does not support the rocking feature of FanControl. - local fan_name = create_fan_profile(device) - fan_name = string.gsub(fan_name, "-rock", "") - profile_name = profile_name .. fan_name - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "" then - profile_name = profile_name .. "-heating-cooling" - else - device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") - return - end - - if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then - profile_name = "room-air-conditioner" - end - - if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then - profile_name = profile_name .. "-nostate" - end - - elseif device_type == FAN_DEVICE_TYPE_ID then - profile_name = create_fan_profile(device) - -- remove leading "-" - profile_name = string.sub(profile_name, 2) - if profile_name == "fan" then - profile_name = "fan-generic" - end - - elseif device_type == AP_DEVICE_TYPE_ID then - local fan_eps_found - profile_name, fan_eps_found = create_air_purifier_profile(device) - if #thermostat_eps > 0 then - profile_name = profile_name .. "-thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes ~= "No Heating nor Cooling Support" then - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then - profile_name = profile_name .. "-temperature" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - if fan_eps_found then - profile_name = profile_name .. "-fan" - end - end - profile_name = profile_name .. create_air_quality_sensor_profile(device) - elseif device_type == WATER_HEATER_DEVICE_TYPE_ID then - -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and - -- ElectricalPowerMeasurement clusters. - local electrical_sensor_eps = get_endpoints_for_dt(device, ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} - if #electrical_sensor_eps > 0 then - profile_name = "water-heater-power-energy-powerConsumption" - end - elseif device_type == HEAT_PUMP_DEVICE_TYPE_ID then - profile_name = "heat-pump" - local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 - for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do - profile_name = profile_name .. "-thermostat" - if tbl_contains(humidity_eps, thermostat_eps[i]) then - profile_name = profile_name .. "-humidity" - end - end - elseif #thermostat_eps > 0 then - profile_name = "thermostat" - - if #humidity_eps > 0 then - profile_name = profile_name .. "-humidity" - end - - -- thermostat profiles support neither wind nor rocking FanControl attributes - local fan_name = create_fan_profile(device) - if fan_name ~= "" then - profile_name = profile_name .. "-fan" - end - - local thermostat_modes = create_thermostat_modes_profile(device) - if thermostat_modes == "No Heating nor Cooling Support" then - device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") - return - else - profile_name = profile_name .. thermostat_modes - end - - if not running_state_supported then - profile_name = profile_name .. "-nostate" - end - - if battery_supported == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif battery_supported == battery_support.NO_BATTERY then - profile_name = profile_name .. "-nobattery" - end - else - device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") - return - end - - if profile_name then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - end - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function get_thermostat_optional_capabilities(device) - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - - local supported_thermostat_capabilities = {} - - if #heat_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) - end - - return supported_thermostat_capabilities -end - -local function get_air_quality_optional_capabilities(device) - local supported_air_quality_capabilities = {} - - local measurement_caps, level_caps = supported_level_measurements(device) - - for _, cap_id in ipairs(measurement_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - for _, cap_id in ipairs(level_caps) do - table.insert(supported_air_quality_capabilities, cap_id) - end - - return supported_air_quality_capabilities -end - -local function match_modular_profile_air_purifer(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local hepa_filter_component_capabilities = {} - local ac_filter_component_capabilties = {} - local profile_name = "air-purifier-modular" - - local MAIN_COMPONENT_IDX = 1 - local CAPABILITIES_LIST_IDX = 2 - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - - local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) - local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) - - if #hepa_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) - end - - table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) - end - if #ac_filter_eps > 0 then - local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) - if #filter_state_eps > 0 then - table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) - end - - table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) - end - - -- determine fan capabilities, note that airPurifierFanMode is already mandatory - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - - if #thermostat_eps > 0 then - -- thermostatMode and temperatureMeasurement should be expected if thermostat is present - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - - -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support - if #temp_eps == 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - end - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - end - - local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - if #aqs_eps > 0 then - table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) - end - - local supported_air_quality_capabilities = get_air_quality_optional_capabilities(device) - for _, capability_id in pairs(supported_air_quality_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - if #ac_filter_component_capabilties > 0 then - table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) - end - if #hepa_filter_component_capabilities > 0 then - table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) - end - - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- 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 - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) - table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.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) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile_thermostat(driver, device) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "thermostat-modular" - - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanMode.ID) - end - if #rock_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local thermostat_capabilities = get_thermostat_optional_capabilities(device) - for _, capability_id in pairs(thermostat_capabilities) do - table.insert(main_component_capabilities, capability_id) - end - - local battery_supported = device:get_field(profiling_data.BATTERY_SUPPORT) - if battery_supported == battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif battery_supported == battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- 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 - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile_room_ac(driver, device) - local running_state_supported = device:get_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) - local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name = "room-air-conditioner-modular" - - if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) - end - - -- determine fan capabilities - local fan_eps = device:get_endpoints(clusters.FanControl.ID) - local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) - -- Note: Room AC does not support the rocking feature of FanControl. - - if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) - table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) - end - if #wind_eps > 0 then - table.insert(main_component_capabilities, capabilities.windMode.ID) - end - - local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) - local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) - - if #heat_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) - end - if #cool_eps > 0 then - table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) - end - - if running_state_supported then - table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - - -- 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 - -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities - table.insert(main_component_capabilities, capabilities.switch.ID) - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) - table.insert(main_component_capabilities, capabilities.thermostatMode.ID) - table.insert(main_component_capabilities, capabilities.refresh.ID) - table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) - - device:set_field(SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) - end -end - -local function match_modular_profile(driver, device, device_type) - if profiling_data_still_required(device) then return end - - if device_type == AP_DEVICE_TYPE_ID then - match_modular_profile_air_purifer(driver, device) - elseif device_type == RAC_DEVICE_TYPE_ID then - match_modular_profile_room_ac(driver, device) - elseif device_type == THERMOSTAT_DEVICE_TYPE_ID then - match_modular_profile_thermostat(driver, device) - else - device.log.warn_with({hub_logs=true}, "Device type is not supported by modular profile in thermostat driver, trying profile switch instead") - match_profile_switch(driver, device) - return - end - - -- clear all profiling data fields after profiling is complete. - for _, field in pairs(profiling_data) do - device:set_field(field, nil) - end -end - -local function supports_modular_profile(device) - local supported_modular_device_types = { - AP_DEVICE_TYPE_ID, - RAC_DEVICE_TYPE_ID, - THERMOSTAT_DEVICE_TYPE_ID, - } - local device_type = get_device_type(device) - if not tbl_contains(supported_modular_device_types, device_type) then - device_type = false - end - return version.api >= 14 and version.rpc >= 8 and device_type -end - -function match_profile(driver, device) - local modular_device_type = supports_modular_profile(device) - if modular_device_type then - match_modular_profile(driver, device, modular_device_type) - else - match_profile_switch(driver, device) - end -end - -local function do_configure(driver, device) - match_profile(driver, device) -end - -local function driver_switched(driver, device) - match_profile(driver, device) -end - -local function device_added(driver, device) - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) - req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) - req:merge(clusters.FanControl.attributes.WindSupport:read(device)) - req:merge(clusters.FanControl.attributes.RockSupport:read(device)) - - local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) - if #thermostat_eps > 0 then - req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - end - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps > 0 then - req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY) - end - device:send(req) - local heat_pump_eps = get_endpoints_for_dt(device, HEAT_PUMP_DEVICE_TYPE_ID) or {} - if #heat_pump_eps > 0 then - local thermostat_eps = get_endpoints_for_dt(device, THERMOSTAT_DEVICE_TYPE_ID) or {} - local component_to_endpoint_map = { - ["thermostatOne"] = thermostat_eps[1], - ["thermostatTwo"] = thermostat_eps[2], - } - device:set_field(COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) - end -end - -local function store_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local units = { - PPM = 0, - PPB = 1, - PPT = 2, - MGM3 = 3, - UGM3 = 4, - NGM3 = 5, - PM3 = 6, - BQM3 = 7, - PCIL = 0xFF -- not in matter spec -} - -local unit_strings = { - [units.PPM] = "ppm", - [units.PPB] = "ppb", - [units.PPT] = "ppt", - [units.MGM3] = "mg/m^3", - [units.NGM3] = "ng/m^3", - [units.UGM3] = "μg/m^3", - [units.BQM3] = "Bq/m^3", - [units.PCIL] = "pCi/L" -} - -local unit_default = { - [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, - [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, - [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, - [capabilities.ozoneMeasurement.NAME] = units.PPM, - [capabilities.formaldehydeMeasurement.NAME] = units.PPM, - [capabilities.veryFineDustSensor.NAME] = units.UGM3, - [capabilities.fineDustSensor.NAME] = units.UGM3, - [capabilities.dustSensor.NAME] = units.UGM3, - [capabilities.radonMeasurement.NAME] = units.BQM3, - [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb -} - --- All ConcentrationMesurement clusters inherit from the same base cluster definitions, --- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist --- in all ConcentrationMeasurement clusters -local level_strings = { - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", - [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", -} - --- measured in g/mol -local 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 conversion_tables = { - [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end, - [units.UGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight * 10^3) / MGM3_PPM_CONVERSION_FACTOR) end, - [units.MGM3] = function(value, molecular_weight) return utils.round((value * molecular_weight) / MGM3_PPM_CONVERSION_FACTOR) end, - }, - [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end, - }, - [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end - }, - [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, - }, - [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end, - [units.PPM] = function(value, molecular_weight) return utils.round((value * MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, - }, - [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end - }, - [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - }, -} - -local function unit_conversion(value, from_unit, to_unit, capability_name) - local conversion_function = conversion_tables[from_unit][to_unit] - if not conversion_function then - log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", unit_strings[from_unit], unit_strings[to_unit])) - return - end - - if not value then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return - end - - return conversion_function(value, molecular_weights[capability_name]) -end - -local function measurementHandlerFactory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if not reporting_unit then - reporting_unit = unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - local value = nil - if reporting_unit then - value = 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 = 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 = unit_strings[target_unit]})) - end - end - end -end - -local function levelHandlerFactory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(level_strings[ib.data.value])) - end -end - --- handlers -local function air_quality_attr_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -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.switch.switch.on()) - else - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) - end -end - -local function temp_event_handler(attribute) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local unit = "C" - - -- Only emit the capability for RPC version >= 5, since unit conversion for - -- range capabilities is only supported in that case. - if version.rpc >= 5 then - local event - if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_COOL) or THERMOSTAT_MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_COOL) or THERMOSTAT_MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - - local range = { - minimum = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, - maximum = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, - step = 0.1 - } - event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - end - end - - local temp = ib.data.value / 100.0 - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) - end -end - -local temp_attr_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local temp = ib.data.value / 100.0 - local unit = "C" - temp = utils.clamp_value(temp, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) - local min = get_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) - local max = get_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- temperature range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) - end - set_field_for_endpoint(device, setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) - set_field_for_endpoint(device, setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) - end - end - end -end - -local function humidity_attr_handler(driver, device, ib, response) - local humidity = math.floor(ib.data.value / 100.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) -end - -local function system_mode_handler(driver, device, ib, response) - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the sequence_of_operation_handler hasn't run. - device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") - device:set_field(SAVED_SYSTEM_MODE_IB, ib) - return - end - - local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} - -- check that the given mode was in the supported modes list - if tbl_contains(supported_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) - return - end - -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. - local disallowed_thermostat_modes = device:get_field(DISALLOWED_THERMOSTAT_MODES) or {} - if tbl_contains(disallowed_thermostat_modes, THERMOSTAT_MODE_MAP[ib.data.value].NAME) then - return - end - -- if we get here, then the reported mode is allowed and not in our mode map - -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables - local optional_modes_seen = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} - table.insert(optional_modes_seen, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) - local sm_copy = utils.deep_copy(supported_modes) - table.insert(sm_copy, THERMOSTAT_MODE_MAP[ib.data.value].NAME) - local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) - device:emit_event_for_endpoint(ib.endpoint_id, THERMOSTAT_MODE_MAP[ib.data.value]()) -end - -local function running_state_handler(driver, device, ib, response) - for mode, operating_state in pairs(THERMOSTAT_OPERATING_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) -end - -local function sequence_of_operation_handler(driver, device, ib, response) - -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. - -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. - -- We also assert that Off is supported, though per spec this is optional. - if device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then - device:set_field(OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) - end - local supported_modes = utils.deep_copy(device:get_field(OPTIONAL_THERMOSTAT_MODES_SEEN)) - local disallowed_mode_operations = {} - - local modes_for_inclusion = {} - if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then - local _, found_idx = tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) - if found_idx then - table.remove(supported_modes, found_idx) -- if seen before, remove now - end - table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) - elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) - end - - -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation - local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) - if #auto > 0 then - table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) - else - table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) - end - - -- if a disallowed value was once allowed and added, it should be removed now. - for index, mode in pairs(supported_modes) do - if tbl_contains(disallowed_mode_operations, mode) then - table.remove(supported_modes, index) - end - end - -- do not include any values twice - for _, mode in pairs(modes_for_inclusion) do - if not tbl_contains(supported_modes, mode) then - table.insert(supported_modes, mode) - end - end - device:set_field(DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) - local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) - - -- will be set by the SystemMode handler if this handler hasn't run yet. - if device:get_field(SAVED_SYSTEM_MODE_IB) then - system_mode_handler(driver, device, device:get_field(SAVED_SYSTEM_MODE_IB), response) - device:set_field(SAVED_SYSTEM_MODE_IB, nil) - end -end - -local function min_deadband_limit_handler(driver, device, ib, response) - local val = ib.data.value / 10.0 - log.info("Setting " .. setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) - device:set_field(setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) - device:set_field(setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) -end - -local function fan_mode_handler(driver, device, ib, response) - local fan_mode_event = { - [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), - capabilities.airConditionerFanMode.fanMode("off"), - capabilities.airPurifierFanMode.airPurifierFanMode.off(), - nil }, -- 'OFF' is not supported by thermostatFanMode - [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), - capabilities.airConditionerFanMode.fanMode("low"), - capabilities.airPurifierFanMode.airPurifierFanMode.low(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), - capabilities.airConditionerFanMode.fanMode("medium"), - capabilities.airPurifierFanMode.airPurifierFanMode.medium(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), - capabilities.airConditionerFanMode.fanMode("high"), - capabilities.airPurifierFanMode.airPurifierFanMode.high(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.on() }, - [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() }, - [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), - capabilities.airConditionerFanMode.fanMode("auto"), - capabilities.airPurifierFanMode.airPurifierFanMode.auto(), - capabilities.thermostatFanMode.thermostatFanMode.auto() } - } - local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or - device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or - device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or - device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 - if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then - device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) - else - log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) - end -end - -local function fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes, supported_fan_modes_attribute - if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { "off", "low", "medium", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { "off", "low", "high" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { "off", "low", "medium", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { "off", "low", "high", "auto" } - elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then - supportedFanModes = { "off", "high", "auto" } - else - supportedFanModes = { "off", "high" } - end - - if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then - supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes - elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then - supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes - elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then - supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes - -- Our thermostat fan mode control is not granular enough to handle all of the supported modes - if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and - ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { "auto", "on" } - else - supportedFanModes = { "on" } - end - else - supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes - end - - local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function fan_speed_percent_attr_handler(driver, device, ib, response) - local speed = 0 - if ib.data.value ~= nil then - speed = utils.clamp_value(ib.data.value, MIN_ALLOWED_PERCENT_VALUE, MAX_ALLOWED_PERCENT_VALUE) - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) -end - -local function wind_support_handler(driver, device, ib, response) - local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} - for mode, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_wind_modes, wind_mode.NAME) - end - end - local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function wind_setting_handler(driver, device, ib, response) - for index, wind_mode in pairs(WIND_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) -end - -local function rock_support_handler(driver, device, ib, response) - local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} - for mode, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> mode) & 1) > 0 then - table.insert(supported_rock_modes, rock_mode.NAME) - end - end - local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end - -local function rock_setting_handler(driver, device, ib, response) - for index, rock_mode in pairs(ROCK_MODE_MAP) do - if ((ib.data.value >> index) & 1) > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) - return - end - end - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) -end - -local function hepa_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function hepa_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["hepaFilter"] - if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function activated_carbon_filter_condition_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - local condition = ib.data.value - device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) -end - -local function activated_carbon_filter_change_indication_handler(driver, device, ib, response) - local component = device.profile.components["activatedCarbonFilter"] - if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) - elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then - device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) - end -end - -local function handle_switch_on(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.On(device, endpoint_id) - device:send(req) -end - -local function handle_switch_off(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.OnOff.ID) - local req = clusters.OnOff.server.commands.Off(device, endpoint_id) - device:send(req) -end - -local function set_thermostat_mode(driver, device, cmd) - local mode_id = nil - for value, mode in pairs(THERMOSTAT_MODE_MAP) do - if mode.NAME == cmd.args.mode then - mode_id = value - break - end - end - if mode_id then - device:send(clusters.Thermostat.attributes.SystemMode:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) - end -end - -local thermostat_mode_setter = function(mode_name) - return function(driver, device, cmd) - return set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) - end -end - -local function set_setpoint(setpoint) - return function(driver, device, cmd) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local value = cmd.args.setpoint - if version.rpc <= 5 and value > MAX_TEMP_IN_C then - value = utils.f_to_c(value) - end - - -- Gather cached setpoint values when considering setpoint limits - -- Note: cached values should always exist, but defaults are chosen just in case to prevent - -- nil operation errors, and deadband logic from triggering. - local cached_cooling_val, cooling_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatCoolingSetpoint.ID, - capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, - MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } - ) - if cooling_setpoint and cooling_setpoint.unit == "F" then - cached_cooling_val = utils.f_to_c(cached_cooling_val) - end - local cached_heating_val, heating_setpoint = device:get_latest_state( - cmd.component, capabilities.thermostatHeatingSetpoint.ID, - capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, - MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } - ) - if heating_setpoint and heating_setpoint.unit == "F" then - cached_heating_val = utils.f_to_c(cached_heating_val) - end - local is_auto_capable = #device:get_endpoints( - clusters.Thermostat.ID, - {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} - ) > 0 - - --Check setpoint limits for the device - local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" - local deadband = device:get_field(setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default - if setpoint_type == "Heat" then - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - if is_auto_capable and value > (cached_cooling_val - deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", - value, cooling_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) - return - end - else - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C - if value < min or value > max then - log.warn(string.format( - "Invalid setpoint (%s) outside the min (%s) and the max (%s)", - value, min, max - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - if is_auto_capable and value < (cached_heating_val + deadband) then - log.warn(string.format( - "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", - value, heating_setpoint, deadband - )) - device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) - return - end - end - device:send(setpoint:write(device, component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), utils.round(value * 100.0))) - end -end - -local heating_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local MAX_TEMP_IN_C = THERMOSTAT_MAX_TEMP_IN_C - local MIN_TEMP_IN_C = THERMOSTAT_MIN_TEMP_IN_C - local is_water_heater_device = (get_device_type(device) == WATER_HEATER_DEVICE_TYPE_ID) - if is_water_heater_device then - MAX_TEMP_IN_C = WATER_HEATER_MAX_TEMP_IN_C - MIN_TEMP_IN_C = WATER_HEATER_MIN_TEMP_IN_C - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_HEAT) - local max = device:get_field(setpoint_limit_device_field.MAX_HEAT) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- heating setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end - -local cooling_setpoint_limit_handler_factory = function(minOrMax) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local val = ib.data.value / 100.0 - val = utils.clamp_value(val, THERMOSTAT_MIN_TEMP_IN_C, THERMOSTAT_MAX_TEMP_IN_C) - device:set_field(minOrMax, val) - local min = device:get_field(setpoint_limit_device_field.MIN_COOL) - local max = device:get_field(setpoint_limit_device_field.MAX_COOL) - if min ~= nil and max ~= nil then - if min < max then - -- Only emit the capability for RPC version >= 5 (unit conversion for - -- cooling setpoint range capability is only supported for RPC >= 5) - if version.rpc >= 5 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) - end - else - device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) - end - end - end -end - -local function set_fan_mode(device, cmd, fan_mode_capability) - local command_argument = cmd.args.fanMode - if fan_mode_capability == capabilities.airPurifierFanMode then - command_argument = cmd.args.airPurifierFanMode - elseif fan_mode_capability == capabilities.thermostatFanMode then - command_argument = cmd.args.mode - end - local fan_mode_id - if command_argument == "off" then - fan_mode_id = clusters.FanControl.attributes.FanMode.OFF - elseif command_argument == "on" then - fan_mode_id = clusters.FanControl.attributes.FanMode.ON - elseif command_argument == "auto" then - fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO - elseif command_argument == "high" then - fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH - elseif command_argument == "medium" then - fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM - elseif tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then - fan_mode_id = clusters.FanControl.attributes.FanMode.LOW - else - device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) - return - end - device:send(clusters.FanControl.attributes.FanMode:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) -end - -local set_fan_mode_factory = function(fan_mode_capability) - return function(driver, device, cmd) - set_fan_mode(device, cmd, fan_mode_capability) - end -end +local log = require "log" +local version = require "version" +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 attribute_handlers = require "thermostat_handlers.attribute_handlers" +local capability_handlers = require "thermostat_handlers.capability_handlers" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" -local function thermostat_fan_mode_setter(mode_name) - return function(driver, device, cmd) - set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) - end +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end -local function set_fan_speed_percent(driver, device, cmd) - local speed = math.floor(cmd.args.percent) - device:send(clusters.FanControl.attributes.PercentSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end -local function set_wind_mode(driver, device, cmd) - local wind_mode = 0 - if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND - elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then - wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND - end - device:send(clusters.FanControl.attributes.WindSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end -local function set_rock_mode(driver, device, cmd) - local rock_mode = 0 - if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN - elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then - rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND - end - device:send(clusters.FanControl.attributes.RockSetting:write(device, component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) -end +local ThermostatLifecycleHandlers = {} -local function set_water_heater_mode(driver, device, cmd) - device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) - local endpoint_id = component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) - local supportedWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do - if cmd.args.mode == mode[2] then - device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) - return - end - end -end +function ThermostatLifecycleHandlers.device_added(driver, device) + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.Thermostat.attributes.ControlSequenceOfOperation:read(device)) + req:merge(clusters.FanControl.attributes.FanModeSequence:read(device)) + req:merge(clusters.FanControl.attributes.WindSupport:read(device)) + req:merge(clusters.FanControl.attributes.RockSupport:read(device)) -local function reset_filter_state(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - if cmd.component == "hepaFilter" then - device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + if #thermostat_eps > 0 then + req:merge(clusters.Thermostat.attributes.AttributeList:read(device)) else - device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) end -end - -local function battery_percent_remaining_attr_handler(driver, device, ib, response) - if ib.data.value then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps > 0 then + req:merge(clusters.PowerSource.attributes.AttributeList:read(device)) + else + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY) end -end - -local function active_power_handler(driver, device, ib, response) - if ib.data.value then - local watt_value = ib.data.value / 1000 - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end + device:send(req) + local heat_pump_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.HEAT_PUMP_DEVICE_TYPE_ID) or {} + if #heat_pump_eps > 0 then + local thermostat_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.THERMOSTAT_DEVICE_TYPE_ID) or {} + local component_to_endpoint_map = { + ["thermostatOne"] = thermostat_eps[1], + ["thermostatTwo"] = thermostat_eps[2], + } + device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_to_endpoint_map, {persist = true}) end end -local function periodic_energy_imported_handler(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local energy_imported_Wh = utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) - report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) - end +function ThermostatLifecycleHandlers.do_configure(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function cumulative_energy_imported_handler(driver, device, ib, response) - if ib.data then - if version.api < 11 then - clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) - end - local endpoint_id = string.format(ib.endpoint_id) - local cumulative_energy_imported = device:get_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} - local cumulative_energy_imported_Wh = utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh - cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh - device:set_field(TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) - local total_cumulative_energy_imported = get_total_cumulative_energy_imported(device) - device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) - report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) - end +function ThermostatLifecycleHandlers.driver_switched(driver, device) + local device_cfg = require "thermostat_utils.device_configuration" + device_cfg.match_profile(device) end -local function energy_report_handler_factory(is_cumulative_report) - return function(driver, device, ib, response) - if is_cumulative_report then - cumulative_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(CUMULATIVE_REPORTS_NOT_SUPPORTED) then - periodic_energy_imported_handler(driver, device, ib, response) - end +function ThermostatLifecycleHandlers.device_init(driver, device) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then + -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id + -- library function to utilize optional capabilities + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end - -local function water_heater_supported_modes_attr_handler(driver, device, ib, response) - local supportWaterHeaterModes = {} - local supportWaterHeaterModesWithIdx = {} - for _, mode in ipairs(ib.data.elements) do - if version.api < 13 then - clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + device:subscribe() + device:set_component_to_endpoint_fn(thermostat_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(thermostat_utils.endpoint_to_component) + if not device:get_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then + local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + --Query min setpoint deadband if needed + if #auto_eps ~= 0 and device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) == nil then + device:send(clusters.Thermostat.attributes.MinSetpointDeadBand:read()) end - table.insert(supportWaterHeaterModes, mode.elements.label.value) - table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) end - device:set_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) - local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) - event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) - device:emit_event_for_endpoint(ib.endpoint_id, event) -end -local function water_heater_mode_handler(driver, device, ib, response) - device.log.info(string.format("water_heater_mode_handler mode: %s", ib.data.value)) - local supportWaterHeaterModesWithIdx = device:get_field(SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} - local currentMode = ib.data.value - for i, mode in ipairs(supportWaterHeaterModesWithIdx) do - if mode[1] == currentMode then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) - break - end + -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. + local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) + if #electrical_energy_measurement_eps > 0 then + local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + device, + clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} + ) + if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end end end -local function battery_charge_level_attr_handler(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()) +function ThermostatLifecycleHandlers.info_changed(driver, device, event, args) + if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + -- This indicates the device should be using a modular profile, so + -- re-up subscription with new capabilities using the modular supports_capability override + device:extend_device("supports_capability_by_id", thermostat_utils.supports_capability_by_id_modular) end -end -local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. - if attr.value == 0x0C then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE) - match_profile(driver, device) - return - elseif attr.value == 0x0E then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL) - match_profile(driver, device) - return - end + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() end end -local function thermostat_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do - -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling - if attr.value == 0x029 then - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) - match_profile(driver, device) - return - end - end - device:set_field(profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) - match_profile(driver, device) +function ThermostatLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") end local matter_driver_template = { lifecycle_handlers = { - init = device_init, - added = device_added, - doConfigure = do_configure, - infoChanged = info_changed, - removed = device_removed, - driverSwitched = driver_switched + added = ThermostatLifecycleHandlers.device_added, + doConfigure = ThermostatLifecycleHandlers.do_configure, + driverSwitched = ThermostatLifecycleHandlers.driver_switched, + infoChanged = ThermostatLifecycleHandlers.info_changed, + init = ThermostatLifecycleHandlers.device_init, + removed = ThermostatLifecycleHandlers.device_removed, }, matter_handlers = { attr = { - [clusters.OnOff.ID] = { - [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + [clusters.ActivatedCarbonFilterMonitoring.ID] = { + [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.activated_carbon_filter_change_indication_handler, + [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = attribute_handlers.activated_carbon_filter_condition_handler, }, - [clusters.Thermostat.ID] = { - [clusters.Thermostat.attributes.LocalTemperature.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = temp_event_handler(capabilities.thermostatCoolingSetpoint.coolingSetpoint), - [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = temp_event_handler(capabilities.thermostatHeatingSetpoint.heatingSetpoint), - [clusters.Thermostat.attributes.SystemMode.ID] = system_mode_handler, - [clusters.Thermostat.attributes.ThermostatRunningState.ID] = running_state_handler, - [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = sequence_of_operation_handler, - [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_HEAT), - [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = heating_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_HEAT), - [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MIN_COOL), - [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = cooling_setpoint_limit_handler_factory(setpoint_limit_device_field.MAX_COOL), - [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = min_deadband_limit_handler, - [clusters.Thermostat.attributes.AttributeList.ID] = thermostat_attribute_list_handler, + [clusters.AirQuality.ID] = { + [clusters.AirQuality.attributes.AirQuality.ID] = attribute_handlers.air_quality_handler, + }, + [clusters.ElectricalEnergyMeasurement.ID] = { + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + }, + [clusters.ElectricalPowerMeasurement.ID] = { + [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler }, [clusters.FanControl.ID] = { - [clusters.FanControl.attributes.FanModeSequence.ID] = fan_mode_sequence_handler, - [clusters.FanControl.attributes.FanMode.ID] = fan_mode_handler, - [clusters.FanControl.attributes.PercentCurrent.ID] = fan_speed_percent_attr_handler, - [clusters.FanControl.attributes.WindSupport.ID] = wind_support_handler, - [clusters.FanControl.attributes.WindSetting.ID] = wind_setting_handler, - [clusters.FanControl.attributes.RockSupport.ID] = rock_support_handler, - [clusters.FanControl.attributes.RockSetting.ID] = rock_setting_handler, + [clusters.FanControl.attributes.FanMode.ID] = attribute_handlers.fan_mode_handler, + [clusters.FanControl.attributes.FanModeSequence.ID] = attribute_handlers.fan_mode_sequence_handler, + [clusters.FanControl.attributes.PercentCurrent.ID] = attribute_handlers.percent_current_handler, + [clusters.FanControl.attributes.RockSetting.ID] = attribute_handlers.rock_setting_handler, + [clusters.FanControl.attributes.RockSupport.ID] = attribute_handlers.rock_support_handler, + [clusters.FanControl.attributes.WindSetting.ID] = attribute_handlers.wind_setting_handler, + [clusters.FanControl.attributes.WindSupport.ID] = attribute_handlers.wind_support_handler, }, - [clusters.TemperatureMeasurement.ID] = { - [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = temp_event_handler(capabilities.temperatureMeasurement.temperature), - [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MIN_TEMP), - [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = temp_attr_handler_factory(setpoint_limit_device_field.MAX_TEMP), + [clusters.HepaFilterMonitoring.ID] = { + [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = attribute_handlers.hepa_filter_change_indication_handler, + [clusters.HepaFilterMonitoring.attributes.Condition.ID] = attribute_handlers.hepa_filter_condition_handler, }, - [clusters.RelativeHumidityMeasurement.ID] = { - [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = humidity_attr_handler + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = attribute_handlers.on_off_handler, }, [clusters.PowerSource.ID] = { - [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, - [clusters.PowerSource.attributes.BatChargeLevel.ID] = battery_charge_level_attr_handler, - [clusters.PowerSource.attributes.BatPercentRemaining.ID] = battery_percent_remaining_attr_handler, + [clusters.PowerSource.attributes.AttributeList.ID] = attribute_handlers.power_source_attribute_list_handler, + [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, + [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, - [clusters.HepaFilterMonitoring.ID] = { - [clusters.HepaFilterMonitoring.attributes.Condition.ID] = hepa_filter_condition_handler, - [clusters.HepaFilterMonitoring.attributes.ChangeIndication.ID] = hepa_filter_change_indication_handler + [clusters.RelativeHumidityMeasurement.ID] = { + [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, - [clusters.ActivatedCarbonFilterMonitoring.ID] = { - [clusters.ActivatedCarbonFilterMonitoring.attributes.Condition.ID] = activated_carbon_filter_condition_handler, - [clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.ID] = activated_carbon_filter_change_indication_handler + [clusters.TemperatureMeasurement.ID] = { + [clusters.TemperatureMeasurement.attributes.MaxMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MAX_TEMP), + [clusters.TemperatureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.TemperatureMeasurement.attributes.MinMeasuredValue.ID] = attribute_handlers.temperature_measured_value_bounds_factory(fields.setpoint_limit_device_field.MIN_TEMP), }, - [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = air_quality_attr_handler, + [clusters.Thermostat.ID] = { + [clusters.Thermostat.attributes.AttributeList.ID] = attribute_handlers.thermostat_attribute_list_handler, + [clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_COOL), + [clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MAX_HEAT), + [clusters.Thermostat.attributes.AbsMinCoolSetpointLimit.ID] = attribute_handlers.abs_cool_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_COOL), + [clusters.Thermostat.attributes.AbsMinHeatSetpointLimit.ID] = attribute_handlers.abs_heat_setpoint_limit_factory(fields.setpoint_limit_device_field.MIN_HEAT), + [clusters.Thermostat.attributes.ControlSequenceOfOperation.ID] = attribute_handlers.control_sequence_of_operation_handler, + [clusters.Thermostat.attributes.LocalTemperature.ID] = attribute_handlers.temperature_handler_factory(capabilities.temperatureMeasurement.temperature), + [clusters.Thermostat.attributes.MinSetpointDeadBand.ID] = attribute_handlers.min_setpoint_deadband_handler, + [clusters.Thermostat.attributes.OccupiedCoolingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatCoolingSetpoint.coolingSetpoint), + [clusters.Thermostat.attributes.OccupiedHeatingSetpoint.ID] = attribute_handlers.temperature_handler_factory(capabilities.thermostatHeatingSetpoint.heatingSetpoint), + [clusters.Thermostat.attributes.SystemMode.ID] = attribute_handlers.system_mode_handler, + [clusters.Thermostat.attributes.ThermostatRunningState.ID] = attribute_handlers.thermostat_running_state_handler, }, - [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.WaterHeaterMode.ID] = { + [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = attribute_handlers.water_heater_current_mode_handler, + [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = attribute_handlers.water_heater_supported_modes_handler }, + -- CONCENTRATION MEASUREMENT CLUSTERS -- [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + }, + [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + }, + [clusters.FormaldehydeConcentrationMeasurement.ID] = { + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), }, [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), }, [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.ozoneHealthConcern.ozoneHealthConcern) - }, - [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern), + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.ozoneMeasurement.NAME), }, [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), - }, - [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.veryFineDustSensor.NAME), }, [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.dustSensor.NAME), + }, + [clusters.Pm25ConcentrationMeasurement.ID] = { + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.fineDustSensor.NAME), }, [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.radonHealthConcern.radonHealthConcern) + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.radonHealthConcern.radonHealthConcern), + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.radonMeasurement.NAME), }, [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = measurementHandlerFactory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = store_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = levelHandlerFactory(capabilities.tvocHealthConcern.tvocHealthConcern) - }, - [clusters.ElectricalPowerMeasurement.ID] = { - [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = active_power_handler - }, - [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = energy_report_handler_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = energy_report_handler_factory(false), - }, - [clusters.WaterHeaterMode.ID] = { - [clusters.WaterHeaterMode.attributes.CurrentMode.ID] = water_heater_mode_handler, - [clusters.WaterHeaterMode.attributes.SupportedModes.ID] = water_heater_supported_modes_attr_handler + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.concentration_level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.concentration_measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.concentration_measurement_unit_factory(capabilities.tvocMeasurement.NAME), }, }, }, - subscribed_attributes = subscribed_attributes, capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = handle_switch_on, - [capabilities.switch.commands.off.NAME] = handle_switch_off, + [capabilities.airConditionerFanMode.ID] = { + [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airConditionerFanMode) }, - [capabilities.thermostatMode.ID] = { - [capabilities.thermostatMode.commands.setThermostatMode.NAME] = set_thermostat_mode, - [capabilities.thermostatMode.commands.auto.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.auto.NAME), - [capabilities.thermostatMode.commands.off.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.off.NAME), - [capabilities.thermostatMode.commands.cool.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.cool.NAME), - [capabilities.thermostatMode.commands.heat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.heat.NAME), - [capabilities.thermostatMode.commands.emergencyHeat.NAME] = thermostat_mode_setter(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + [capabilities.airPurifierFanMode.ID] = { + [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.airPurifierFanMode) }, - [capabilities.thermostatFanMode.ID] = { - [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = set_fan_mode_factory(capabilities.thermostatFanMode), - [capabilities.thermostatFanMode.commands.fanAuto.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), - [capabilities.thermostatFanMode.commands.fanOn.NAME] = thermostat_fan_mode_setter(capabilities.thermostatFanMode.thermostatFanMode.on.NAME) + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.fanMode) + }, + [capabilities.fanOscillationMode.ID] = { + [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = capability_handlers.handle_set_fan_oscillation_mode, + }, + [capabilities.fanSpeedPercent.ID] = { + [capabilities.fanSpeedPercent.commands.setPercent.NAME] = capability_handlers.handle_fan_speed_set_percent, + }, + [capabilities.filterState.ID] = { + [capabilities.filterState.commands.resetFilter.NAME] = capability_handlers.handle_filter_state_reset_filter, + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capability_handlers.handle_set_mode, + }, + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, + [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, }, [capabilities.thermostatCoolingSetpoint.ID] = { - [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) + [capabilities.thermostatCoolingSetpoint.commands.setCoolingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedCoolingSetpoint) }, [capabilities.thermostatHeatingSetpoint.ID] = { - [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = set_setpoint(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = capability_handlers.thermostat_set_setpoint_factory(clusters.Thermostat.attributes.OccupiedHeatingSetpoint) + }, + [capabilities.thermostatFanMode.ID] = { + [capabilities.thermostatFanMode.commands.fanAuto.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.auto.NAME), + [capabilities.thermostatFanMode.commands.fanOn.NAME] = capability_handlers.thermostat_fan_mode_command_factory(capabilities.thermostatFanMode.thermostatFanMode.on.NAME), + [capabilities.thermostatFanMode.commands.setThermostatFanMode.NAME] = capability_handlers.fan_mode_command_factory(capabilities.thermostatFanMode), + }, + [capabilities.thermostatMode.ID] = { + [capabilities.thermostatMode.commands.auto.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.auto.NAME), + [capabilities.thermostatMode.commands.cool.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.cool.NAME), + [capabilities.thermostatMode.commands.emergencyHeat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.emergency_heat.NAME), + [capabilities.thermostatMode.commands.heat.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.heat.NAME), + [capabilities.thermostatMode.commands.off.NAME] = capability_handlers.thermostat_mode_command_factory(capabilities.thermostatMode.thermostatMode.off.NAME), + [capabilities.thermostatMode.commands.setThermostatMode.NAME] = capability_handlers.handle_set_thermostat_mode, + }, + [capabilities.windMode.ID] = { + [capabilities.windMode.commands.setWindMode.NAME] = capability_handlers.handle_set_wind_mode, }, + }, + subscribed_attributes = { [capabilities.airConditionerFanMode.ID] = { - [capabilities.airConditionerFanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.airConditionerFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode }, [capabilities.airPurifierFanMode.ID] = { - [capabilities.airPurifierFanMode.commands.setAirPurifierFanMode.NAME] = set_fan_mode_factory(capabilities.airPurifierFanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.battery.ID] = { + clusters.PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + clusters.PowerSource.attributes.BatChargeLevel + }, + [capabilities.energyMeter.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported }, [capabilities.fanMode.ID] = { - [capabilities.fanMode.commands.setFanMode.NAME] = set_fan_mode_factory(capabilities.fanMode) + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.fanOscillationMode.ID] = { + clusters.FanControl.attributes.RockSupport, + clusters.FanControl.attributes.RockSetting }, [capabilities.fanSpeedPercent.ID] = { - [capabilities.fanSpeedPercent.commands.setPercent.NAME] = set_fan_speed_percent, + clusters.FanControl.attributes.PercentCurrent }, - [capabilities.windMode.ID] = { - [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode, + [capabilities.filterState.ID] = { + clusters.HepaFilterMonitoring.attributes.Condition, + clusters.ActivatedCarbonFilterMonitoring.attributes.Condition }, - [capabilities.fanOscillationMode.ID] = { - [capabilities.fanOscillationMode.commands.setFanOscillationMode.NAME] = set_rock_mode, + [capabilities.filterStatus.ID] = { + clusters.HepaFilterMonitoring.attributes.ChangeIndication, + clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication }, [capabilities.mode.ID] = { - [capabilities.mode.commands.setMode.NAME] = set_water_heater_mode, + clusters.WaterHeaterMode.attributes.CurrentMode, + clusters.WaterHeaterMode.attributes.SupportedModes + }, + [capabilities.powerConsumptionReport.ID] = { + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported + }, + [capabilities.powerMeter.ID] = { + clusters.ElectricalPowerMeasurement.attributes.ActivePower + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.switch.ID] = { + clusters.OnOff.attributes.OnOff + }, + [capabilities.temperatureMeasurement.ID] = { + clusters.Thermostat.attributes.LocalTemperature, + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue + }, + [capabilities.thermostatCoolingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedCoolingSetpoint, + clusters.Thermostat.attributes.AbsMinCoolSetpointLimit, + clusters.Thermostat.attributes.AbsMaxCoolSetpointLimit + }, + [capabilities.thermostatFanMode.ID] = { + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + clusters.Thermostat.attributes.OccupiedHeatingSetpoint, + clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, + clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit + }, + [capabilities.thermostatMode.ID] = { + clusters.Thermostat.attributes.SystemMode, + clusters.Thermostat.attributes.ControlSequenceOfOperation + }, + [capabilities.thermostatOperatingState.ID] = { + clusters.Thermostat.attributes.ThermostatRunningState + }, + [capabilities.windMode.ID] = { + clusters.FanControl.attributes.WindSupport, + clusters.FanControl.attributes.WindSetting + }, + -- AIR QUALITY SENSOR DEVICE TYPE SPECIFIC CAPABILITIES -- + [capabilities.airQualityHealthConcern.ID] = { + clusters.AirQuality.attributes.AirQuality + }, + [capabilities.atmosphericPressureMeasurement.ID] = { + clusters.PressureMeasurement.attributes.MeasuredValue + }, + [capabilities.carbonDioxideHealthConcern.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonDioxideMeasurement.ID] = { + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.carbonMonoxideHealthConcern.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.carbonMonoxideMeasurement.ID] = { + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.dustHealthConcern.ID] = { + clusters.Pm10ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.dustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.fineDustHealthConcern.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.fineDustSensor.ID] = { + clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.formaldehydeHealthConcern.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.formaldehydeMeasurement.ID] = { + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue, + clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.nitrogenDioxideHealthConcern.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.nitrogenDioxideMeasurement.ID] = { + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue, + clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.ozoneHealthConcern.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.ozoneMeasurement.ID] = { + clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue, + clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit + }, + [capabilities.radonHealthConcern.ID] = { + clusters.RadonConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.radonMeasurement.ID] = { + clusters.RadonConcentrationMeasurement.attributes.MeasuredValue, + clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.relativeHumidityMeasurement.ID] = { + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue + }, + [capabilities.tvocHealthConcern.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue + }, + [capabilities.tvocMeasurement.ID] = { + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue, + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, + }, + [capabilities.veryFineDustHealthConcern.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.LevelValue, + }, + [capabilities.veryFineDustSensor.ID] = { + clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue, + clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit, }, - [capabilities.filterState.ID] = { - [capabilities.filterState.commands.resetFilter.NAME] = reset_filter_state, - } }, supported_capabilities = { - capabilities.thermostatMode, - capabilities.thermostatHeatingSetpoint, - capabilities.thermostatCoolingSetpoint, - capabilities.thermostatFanMode, - capabilities.thermostatOperatingState, capabilities.airConditionerFanMode, - capabilities.fanMode, - capabilities.fanSpeedPercent, + capabilities.airQualityHealthConcern, capabilities.airPurifierFanMode, - capabilities.windMode, - capabilities.fanOscillationMode, capabilities.battery, capabilities.batteryLevel, - capabilities.filterState, - capabilities.filterStatus, - capabilities.airQualityHealthConcern, capabilities.carbonDioxideHealthConcern, capabilities.carbonDioxideMeasurement, capabilities.carbonMonoxideHealthConcern, capabilities.carbonMonoxideMeasurement, + capabilities.dustHealthConcern, + capabilities.dustSensor, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanOscillationMode, + capabilities.fanSpeedPercent, + capabilities.filterState, + capabilities.filterStatus, + capabilities.fineDustHealthConcern, + capabilities.fineDustSensor, + capabilities.formaldehydeHealthConcern, + capabilities.formaldehydeMeasurement, + capabilities.mode, capabilities.nitrogenDioxideHealthConcern, capabilities.nitrogenDioxideMeasurement, capabilities.ozoneHealthConcern, capabilities.ozoneMeasurement, - capabilities.formaldehydeHealthConcern, - capabilities.formaldehydeMeasurement, - capabilities.veryFineDustHealthConcern, - capabilities.veryFineDustSensor, - capabilities.fineDustHealthConcern, - capabilities.fineDustSensor, - capabilities.dustSensor, - capabilities.dustHealthConcern, + capabilities.powerConsumptionReport, + capabilities.powerMeter, capabilities.radonHealthConcern, capabilities.radonMeasurement, + capabilities.thermostatCoolingSetpoint, + capabilities.thermostatFanMode, + capabilities.thermostatHeatingSetpoint, + capabilities.thermostatMode, + capabilities.thermostatOperatingState, capabilities.tvocHealthConcern, capabilities.tvocMeasurement, - capabilities.powerMeter, - capabilities.energyMeter, - capabilities.powerConsumptionReport, - capabilities.mode + capabilities.veryFineDustHealthConcern, + capabilities.veryFineDustSensor, + capabilities.windMode, }, } 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 d9cb8c6309..ca7fddcdd9 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" @@ -20,19 +10,19 @@ local clusters = require "st.matter.clusters" local version = require "version" if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device = test.mock_device.build_test_matter_device({ 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 871693d5a6..fd68c958d9 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -21,19 +11,19 @@ local version = require "version" version.api = 9 -- include driver-side cluster definitions to test embedded clusters on lower api versions -clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" -clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" -clusters.AirQuality = require "AirQuality" -clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" -clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" -clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" -clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" -clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" -clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" -clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" -clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" -clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" -clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" +clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" +clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +clusters.AirQuality = require "embedded_clusters.AirQuality" +clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" +clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" +clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" +clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" +clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" +clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" +clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" +clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" +clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" +clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("air-purifier-hepa-ac-wind.yml"), 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 4f2b5bd0f4..745076cd04 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -23,19 +12,19 @@ local version = require "version" test.disable_startup_messages() if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end local mock_device_basic = test.mock_device.build_test_matter_device({ @@ -315,10 +304,6 @@ test.register_coroutine_test( "Test profile change on init for basic Air Purifier device", function() test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "doConfigure" }) - test.socket.matter:__queue_receive({ - mock_device_basic.id, - clusters.Thermostat.attributes.AttributeList:build_test_report_data(mock_device_basic, 1, {uint32(0)}) - }) mock_device_basic:expect_metadata_update(expected_update_metadata) mock_device_basic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 4a94a9a7e9..8eefceb23b 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" 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 ba097d0451..e96bfb69fc 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.matter.clusters" @@ -27,8 +16,8 @@ local HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 local THERMOSTAT_DEVICE_TYPE_ID = 0x0301 if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local device_desc = { 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 cec0cb9ffc..35728e628d 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" 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 2da9e5c023..96577d72a3 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright © 2025 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" 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 35f312c67e..9fb8536c23 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 37f73d0e50..c898fab362 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 7f9577fa8f..a8acdc253e 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b28d11dcc7..ae9c25940f 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 72b46af4f3..bbd084cb72 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 @@ -1,15 +1,5 @@ --- --- 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 44ed395797..e40572ff5d 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 e901ac59a4..1f1a4e6cb4 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" 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 6645d39692..73d180da7e 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 b52b08027e..03e7df8ae2 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.matter.clusters" local test = require "integration_test" 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 cbd6464a48..b39db4136b 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local capabilities = require "st.capabilities" @@ -19,11 +8,12 @@ local version = require "version" local clusters = require "st.matter.clusters" if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end local WATER_HEATER_EP = 10 diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua new file mode 100644 index 0000000000..78f5ee3bf2 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -0,0 +1,661 @@ +-- 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" +local st_utils = require "st.utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local AttributeHandlers = {} + + +-- [[ THERMOSTAT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.thermostat_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark whether the optional attribute ThermostatRunningState (0x029) is present and try profiling + if attr.value == 0x029 then + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, true) + device_cfg.match_profile(device) + return + end + end + device:set_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT, false) + device_cfg.match_profile(device) +end + +function AttributeHandlers.system_mode_handler(driver, device, ib, response) + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then -- this being nil means the control_sequence_of_operation_handler hasn't run. + device.log.info_with({hub_logs = true}, "In the SystemMode handler: ControlSequenceOfOperation has not run yet. Exiting early.") + device:set_field(fields.SAVED_SYSTEM_MODE_IB, ib) + return + end + + local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} + -- check that the given mode was in the supported modes list + if thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) + return + end + -- if the value is not found in the supported modes list, check if it's disallowed and early return if so. + local disallowed_thermostat_modes = device:get_field(fields.DISALLOWED_THERMOSTAT_MODES) or {} + if thermostat_utils.tbl_contains(disallowed_thermostat_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then + return + end + -- if we get here, then the reported mode is allowed and not in our mode map + -- add the mode to the OPTIONAL_THERMOSTAT_MODES_SEEN and supportedThermostatModes tables + local optional_modes_seen = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) or {} + table.insert(optional_modes_seen, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, optional_modes_seen, {persist=true}) + local sm_copy = st_utils.deep_copy(supported_modes) + table.insert(sm_copy, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) + local supported_modes_event = capabilities.thermostatMode.supportedThermostatModes(sm_copy, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, supported_modes_event) + device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) +end + +function AttributeHandlers.thermostat_running_state_handler(driver, device, ib, response) + for mode, operating_state in pairs(fields.THERMOSTAT_OPERATING_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, operating_state()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatOperatingState.thermostatOperatingState.idle()) +end + +function AttributeHandlers.control_sequence_of_operation_handler(driver, device, ib, response) + -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. + -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. + -- We also assert that Off is supported, though per spec this is optional. + if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) + end + local supported_modes = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) + local disallowed_mode_operations = {} + + local modes_for_inclusion = {} + if ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.emergency_heat.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.HEATING_WITH_REHEAT then + local _, found_idx = thermostat_utils.tbl_contains(supported_modes, capabilities.thermostatMode.thermostatMode.precooling.NAME) + if found_idx then + table.remove(supported_modes, found_idx) -- if seen before, remove now + end + table.insert(supported_modes, capabilities.thermostatMode.thermostatMode.heat.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.precooling.NAME) + elseif ib.data.value <= clusters.Thermostat.attributes.ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.cool.NAME) + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.heat.NAME) + end + + -- check whether the Auto Mode should be supported in SystemMode, though this is unrelated to ControlSequenceOfOperation + local auto = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) + if #auto > 0 then + table.insert(modes_for_inclusion, capabilities.thermostatMode.thermostatMode.auto.NAME) + else + table.insert(disallowed_mode_operations, capabilities.thermostatMode.thermostatMode.auto.NAME) + end + + -- if a disallowed value was once allowed and added, it should be removed now. + for index, mode in pairs(supported_modes) do + if thermostat_utils.tbl_contains(disallowed_mode_operations, mode) then + table.remove(supported_modes, index) + end + end + -- do not include any values twice + for _, mode in pairs(modes_for_inclusion) do + if not thermostat_utils.tbl_contains(supported_modes, mode) then + table.insert(supported_modes, mode) + end + end + device:set_field(fields.DISALLOWED_THERMOSTAT_MODES, disallowed_mode_operations) + local event = capabilities.thermostatMode.supportedThermostatModes(supported_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + + -- will be set by the SystemMode handler if this handler hasn't run yet. + if device:get_field(fields.SAVED_SYSTEM_MODE_IB) then + AttributeHandlers.system_mode_handler(driver, device, device:get_field(fields.SAVED_SYSTEM_MODE_IB), response) + device:set_field(fields.SAVED_SYSTEM_MODE_IB, nil) + end +end + +function AttributeHandlers.min_setpoint_deadband_handler(driver, device, ib, response) + local val = ib.data.value / 10.0 + log.info("Setting " .. fields.setpoint_limit_device_field.MIN_DEADBAND .. " to " .. string.format("%s", val)) + device:set_field(fields.setpoint_limit_device_field.MIN_DEADBAND, val, { persist = true }) + device:set_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED, true, {persist = true}) +end + +function AttributeHandlers.abs_heat_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = (thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID) + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, MIN_TEMP_IN_C, MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- heating setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min heating setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + +function AttributeHandlers.abs_cool_setpoint_limit_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local val = ib.data.value / 100.0 + val = st_utils.clamp_value(val, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + device:set_field(minOrMax, val) + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- cooling setpoint range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpointRange({ value = { minimum = min, maximum = max, step = 0.1 }, unit = "C" })) + end + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min cooling setpoint %d that is not lower than the reported max %d", min, max)) + end + end + end +end + + +-- [[ TEMPERATURE MEASUREMENT CLUSER ATTRIBUTES ]] -- + +function AttributeHandlers.temperature_handler_factory(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local unit = "C" + + -- Only emit the capability for RPC version >= 5, since unit conversion for + -- range capabilities is only supported in that case. + if version.rpc >= 5 then + local event + if attribute == capabilities.thermostatCoolingSetpoint.coolingSetpoint then + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or fields.THERMOSTAT_MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or fields.THERMOSTAT_MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatCoolingSetpoint.coolingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + elseif attribute == capabilities.thermostatHeatingSetpoint.heatingSetpoint then + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + + local range = { + minimum = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C, + maximum = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C, + step = 0.1 + } + event = capabilities.thermostatHeatingSetpoint.heatingSetpointRange({value = range, unit = unit}) + device:emit_event_for_endpoint(ib.endpoint_id, event) + end + end + + local temp = ib.data.value / 100.0 + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = temp, unit = unit})) + end +end + +function AttributeHandlers.temperature_measured_value_bounds_factory(minOrMax) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local temp = ib.data.value / 100.0 + local unit = "C" + temp = st_utils.clamp_value(temp, fields.THERMOSTAT_MIN_TEMP_IN_C, fields.THERMOSTAT_MAX_TEMP_IN_C) + thermostat_utils.set_field_for_endpoint(device, minOrMax, ib.endpoint_id, temp) + local min = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id) + local max = thermostat_utils.get_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id) + if min ~= nil and max ~= nil then + if min < max then + -- Only emit the capability for RPC version >= 5 (unit conversion for + -- temperature range capability is only supported for RPC >= 5) + if version.rpc >= 5 then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.temperatureMeasurement.temperatureRange({ value = { minimum = min, maximum = max }, unit = unit })) + end + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MIN_TEMP, ib.endpoint_id, nil) + thermostat_utils.set_field_for_endpoint(device, fields.setpoint_limit_device_field.MAX_TEMP, ib.endpoint_id, nil) + else + device.log.warn_with({hub_logs = true}, string.format("Device reported a min temperature %d that is not lower than the reported max temperature %d", min, max)) + end + end + end +end + + +--[[ FAN CONTROL CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.fan_mode_handler(driver, device, ib, response) + local fan_mode_event = { + [clusters.FanControl.attributes.FanMode.OFF] = { capabilities.fanMode.fanMode.off(), + capabilities.airConditionerFanMode.fanMode("off"), + capabilities.airPurifierFanMode.airPurifierFanMode.off(), + nil }, -- 'OFF' is not supported by thermostatFanMode + [clusters.FanControl.attributes.FanMode.LOW] = { capabilities.fanMode.fanMode.low(), + capabilities.airConditionerFanMode.fanMode("low"), + capabilities.airPurifierFanMode.airPurifierFanMode.low(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.MEDIUM] = { capabilities.fanMode.fanMode.medium(), + capabilities.airConditionerFanMode.fanMode("medium"), + capabilities.airPurifierFanMode.airPurifierFanMode.medium(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.HIGH] = { capabilities.fanMode.fanMode.high(), + capabilities.airConditionerFanMode.fanMode("high"), + capabilities.airPurifierFanMode.airPurifierFanMode.high(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.ON] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.on() }, + [clusters.FanControl.attributes.FanMode.AUTO] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() }, + [clusters.FanControl.attributes.FanMode.SMART] = { capabilities.fanMode.fanMode.auto(), + capabilities.airConditionerFanMode.fanMode("auto"), + capabilities.airPurifierFanMode.airPurifierFanMode.auto(), + capabilities.thermostatFanMode.thermostatFanMode.auto() } + } + local fan_mode_idx = device:supports_capability_by_id(capabilities.fanMode.ID) and 1 or + device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) and 2 or + device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) and 3 or + device:supports_capability_by_id(capabilities.thermostatFanMode.ID) and 4 + if fan_mode_idx ~= false and fan_mode_event[ib.data.value][fan_mode_idx] then + device:emit_event_for_endpoint(ib.endpoint_id, fan_mode_event[ib.data.value][fan_mode_idx]) + else + log.warn(string.format("Invalid Fan Mode (%s)", ib.data.value)) + end +end + +function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) + local supportedFanModes, supported_fan_modes_attribute + if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then + supportedFanModes = { "off", "low", "medium", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then + supportedFanModes = { "off", "low", "high" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then + supportedFanModes = { "off", "low", "medium", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then + supportedFanModes = { "off", "low", "high", "auto" } + elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then + supportedFanModes = { "off", "high", "auto" } + else + supportedFanModes = { "off", "high" } + end + + if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then + supported_fan_modes_attribute = capabilities.airPurifierFanMode.supportedAirPurifierFanModes + elseif device:supports_capability_by_id(capabilities.airConditionerFanMode.ID) then + supported_fan_modes_attribute = capabilities.airConditionerFanMode.supportedAcFanModes + elseif device:supports_capability_by_id(capabilities.thermostatFanMode.ID) then + supported_fan_modes_attribute = capabilities.thermostatFanMode.supportedThermostatFanModes + -- Our thermostat fan mode control is not granular enough to handle all of the supported modes + if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and + ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then + supportedFanModes = { "auto", "on" } + else + supportedFanModes = { "on" } + end + else + supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes + end + + local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.percent_current_handler(driver, device, ib, response) + local speed = 0 + if ib.data.value ~= nil then + speed = st_utils.clamp_value(ib.data.value, fields.MIN_ALLOWED_PERCENT_VALUE, fields.MAX_ALLOWED_PERCENT_VALUE) + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(speed)) +end + +function AttributeHandlers.wind_support_handler(driver, device, ib, response) + local supported_wind_modes = {capabilities.windMode.windMode.noWind.NAME} + for mode, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_wind_modes, wind_mode.NAME) + end + end + local event = capabilities.windMode.supportedWindModes(supported_wind_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.wind_setting_handler(driver, device, ib, response) + for index, wind_mode in pairs(fields.WIND_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, wind_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windMode.windMode.noWind()) +end + +function AttributeHandlers.rock_support_handler(driver, device, ib, response) + local supported_rock_modes = {capabilities.fanOscillationMode.fanOscillationMode.off.NAME} + for mode, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> mode) & 1) > 0 then + table.insert(supported_rock_modes, rock_mode.NAME) + end + end + local event = capabilities.fanOscillationMode.supportedFanOscillationModes(supported_rock_modes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.rock_setting_handler(driver, device, ib, response) + for index, rock_mode in pairs(fields.ROCK_MODE_MAP) do + if ((ib.data.value >> index) & 1) > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, rock_mode()) + return + end + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanOscillationMode.fanOscillationMode.off()) +end + + +-- [[ HEPA FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.hepa_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.hepa_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["hepaFilter"] + if ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.HepaFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +-- [[ ACTIVATED CARBON FILTER MONITORING CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.activated_carbon_filter_condition_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + local condition = ib.data.value + device:emit_component_event(component, capabilities.filterState.filterLifeRemaining(condition)) +end + +function AttributeHandlers.activated_carbon_filter_change_indication_handler(driver, device, ib, response) + local component = device.profile.components["activatedCarbonFilter"] + if ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.OK then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.WARNING then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.normal()) + elseif ib.data.value == clusters.ActivatedCarbonFilterMonitoring.attributes.ChangeIndication.CRITICAL then + device:emit_component_event(component, capabilities.filterStatus.filterStatus.replace()) + end +end + + +--[[ AIR QUALITY SENSOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + + +-- [[ CONCENTRATION CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.concentration_measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +function AttributeHandlers.concentration_level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value])) + end +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 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]})) + -- 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]})) + end + end + end +end + + +-- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +function AttributeHandlers.bat_charge_level_handler(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 + +function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) + local device_cfg = require "thermostat_utils.device_configuration" + for _, attr in ipairs(ib.data.elements) do + -- mark if the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present and try profiling. + if attr.value == 0x0C then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE) + device_cfg.match_profile(device) + return + elseif attr.value == 0x0E then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL) + device_cfg.match_profile(device) + return + end + end +end + + +-- [[ ELECTRICAL POWER MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.active_power_handler(driver, device, ib, response) + if ib.data.value then + local watt_value = ib.data.value / 1000 + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W" })) + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") + end + end +end + + +-- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +local function periodic_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local energy_imported_Wh = st_utils.round(ib.data.elements.energy.value / 1000) --convert mWh to Wh + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] or 0 + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported[endpoint_id] + energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], ib.endpoint_id, capabilities.energyMeter.energy({value = total_cumulative_energy_imported, unit = "Wh"})) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +local function cumulative_energy_imported_handler(driver, device, ib, response) + if ib.data then + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:augment_type(ib.data) + end + local endpoint_id = string.format(ib.endpoint_id) + local cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local cumulative_energy_imported_Wh = st_utils.round( ib.data.elements.energy.value / 1000) -- convert mWh to Wh + cumulative_energy_imported[endpoint_id] = cumulative_energy_imported_Wh + device:set_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP, cumulative_energy_imported, { persist = true }) + local total_cumulative_energy_imported = thermostat_utils.get_total_cumulative_energy_imported(device) + device:emit_component_event(device.profile.components["main"], capabilities.energyMeter.energy({ value = total_cumulative_energy_imported, unit = "Wh" })) + thermostat_utils.report_power_consumption_to_st_energy(device, total_cumulative_energy_imported) + end +end + +function AttributeHandlers.energy_imported_factory(is_cumulative_report) + return function(driver, device, ib, response) + if is_cumulative_report then + cumulative_energy_imported_handler(driver, device, ib, response) + elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then + periodic_energy_imported_handler(driver, device, ib, response) + end + end +end + + +-- [[ WATER HEATER MODE CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.water_heater_supported_modes_handler(driver, device, ib, response) + local supportWaterHeaterModes = {} + local supportWaterHeaterModesWithIdx = {} + for _, mode in ipairs(ib.data.elements) do + if version.api < 13 then + clusters.WaterHeaterMode.types.ModeOptionStruct:augment_type(mode) + end + table.insert(supportWaterHeaterModes, mode.elements.label.value) + table.insert(supportWaterHeaterModesWithIdx, {mode.elements.mode.value, mode.elements.label.value}) + end + device:set_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX, supportWaterHeaterModesWithIdx, { persist = true }) + local event = capabilities.mode.supportedModes(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) + event = capabilities.mode.supportedArguments(supportWaterHeaterModes, { visibility = { displayed = false } }) + device:emit_event_for_endpoint(ib.endpoint_id, event) +end + +function AttributeHandlers.water_heater_current_mode_handler(driver, device, ib, response) + device.log.info(string.format("water_heater_current_mode_handler mode: %s", ib.data.value)) + local supportWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + local currentMode = ib.data.value + for i, mode in ipairs(supportWaterHeaterModesWithIdx) do + if mode[1] == currentMode then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.mode.mode(mode[2])) + break + end + end +end + + +-- [[ RELATIVE HUMIDITY MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.relative_humidity_measured_value_handler(driver, device, ib, response) + local humidity = math.floor(ib.data.value / 100.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.relativeHumidityMeasurement.humidity(humidity)) +end + + +-- [[ ON OFF CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.on_off_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.on()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switch.switch.off()) + end +end + +return AttributeHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua new file mode 100644 index 0000000000..4a32b7a700 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/capability_handlers.lua @@ -0,0 +1,256 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local version = require "version" +local st_utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local CapabilityHandlers = {} + + +-- [[ FAN SPEED PERCENT CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) + local speed = math.floor(cmd.args.percent) + device:send(clusters.FanControl.attributes.PercentSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), speed)) +end + + +-- [[ WIND MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_wind_mode(driver, device, cmd) + local wind_mode = 0 + if cmd.args.windMode == capabilities.windMode.windMode.sleepWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.SLEEP_WIND + elseif cmd.args.windMode == capabilities.windMode.windMode.naturalWind.NAME then + wind_mode = clusters.FanControl.types.WindSupportMask.NATURAL_WIND + end + device:send(clusters.FanControl.attributes.WindSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), wind_mode)) +end + + +-- [[ FAN OSCILLATION MODE HANDLERS ]] -- + +function CapabilityHandlers.handle_set_fan_oscillation_mode(driver, device, cmd) + local rock_mode = 0 + if cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.horizontal.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_LEFT_RIGHT + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.vertical.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_UP_DOWN + elseif cmd.args.fanOscillationMode == capabilities.fanOscillationMode.fanOscillationMode.swing.NAME then + rock_mode = clusters.FanControl.types.RockSupportMask.ROCK_ROUND + end + device:send(clusters.FanControl.attributes.RockSetting:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), rock_mode)) +end + + +-- [[ MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_mode(driver, device, cmd) + device.log.info(string.format("set_water_heater_mode mode: %s", cmd.args.mode)) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local supportedWaterHeaterModesWithIdx = device:get_field(fields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX) or {} + for i, mode in ipairs(supportedWaterHeaterModesWithIdx) do + if cmd.args.mode == mode[2] then + device:send(clusters.WaterHeaterMode.commands.ChangeToMode(device, endpoint_id, mode[1])) + return + end + end +end + + +-- [[ FILTER STATE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_filter_state_reset_filter(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + if cmd.component == "hepaFilter" then + device:send(clusters.HepaFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + else + device:send(clusters.ActivatedCarbonFilterMonitoring.server.commands.ResetCondition(device, endpoint_id)) + end +end + + +-- [[ SWITCH CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_switch_on(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.On(device, endpoint_id) + device:send(req) +end + +function CapabilityHandlers.handle_switch_off(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.OnOff.ID) + local req = clusters.OnOff.server.commands.Off(device, endpoint_id) + device:send(req) +end + + +-- [[ THERMOSTAT MODE CAPABLITY HANDLERS ]] -- + +function CapabilityHandlers.handle_set_thermostat_mode(driver, device, cmd) + local mode_id = nil + for value, mode in pairs(fields.THERMOSTAT_MODE_MAP) do + if mode.NAME == cmd.args.mode then + mode_id = value + break + end + end + if mode_id then + device:send(clusters.Thermostat.attributes.SystemMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), mode_id)) + end +end + +function CapabilityHandlers.thermostat_mode_command_factory(mode_name) + return function(driver, device, cmd) + return CapabilityHandlers.handle_set_thermostat_mode(driver, device, {component = cmd.component, args = {mode = mode_name}}) + end +end + + +-- [[ FAN MODE CAPABILITY HANDLERS ]] -- + +local function set_fan_mode(device, cmd, fan_mode_capability) + local command_argument = cmd.args.fanMode + if fan_mode_capability == capabilities.airPurifierFanMode then + command_argument = cmd.args.airPurifierFanMode + elseif fan_mode_capability == capabilities.thermostatFanMode then + command_argument = cmd.args.mode + end + local fan_mode_id + if command_argument == "off" then + fan_mode_id = clusters.FanControl.attributes.FanMode.OFF + elseif command_argument == "on" then + fan_mode_id = clusters.FanControl.attributes.FanMode.ON + elseif command_argument == "auto" then + fan_mode_id = clusters.FanControl.attributes.FanMode.AUTO + elseif command_argument == "high" then + fan_mode_id = clusters.FanControl.attributes.FanMode.HIGH + elseif command_argument == "medium" then + fan_mode_id = clusters.FanControl.attributes.FanMode.MEDIUM + elseif thermostat_utils.tbl_contains({ "low", "sleep", "quiet", "windFree" }, command_argument) then + fan_mode_id = clusters.FanControl.attributes.FanMode.LOW + else + device.log.warn(string.format("Invalid Fan Mode (%s) received from capability command", command_argument)) + return + end + device:send(clusters.FanControl.attributes.FanMode:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.FanControl.ID), fan_mode_id)) +end + +function CapabilityHandlers.fan_mode_command_factory(fan_mode_capability) + return function(driver, device, cmd) + set_fan_mode(device, cmd, fan_mode_capability) + end +end + + +-- [[ THERMOSTAT FAN MODE CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_fan_mode_command_factory(mode_name) + return function(driver, device, cmd) + set_fan_mode(device, {component = cmd.component, args = {mode = mode_name}}, capabilities.thermostatFanMode) + end +end + + +-- [[ THERMOSTAT HEATING/COOLING CAPABILITY HANDLERS ]] -- + +function CapabilityHandlers.thermostat_set_setpoint_factory(setpoint) + return function(driver, device, cmd) + local endpoint_id = thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID) + local MAX_TEMP_IN_C = fields.THERMOSTAT_MAX_TEMP_IN_C + local MIN_TEMP_IN_C = fields.THERMOSTAT_MIN_TEMP_IN_C + local is_water_heater_device = thermostat_utils.get_device_type(device) == fields.WATER_HEATER_DEVICE_TYPE_ID + if is_water_heater_device then + MAX_TEMP_IN_C = fields.WATER_HEATER_MAX_TEMP_IN_C + MIN_TEMP_IN_C = fields.WATER_HEATER_MIN_TEMP_IN_C + end + local value = cmd.args.setpoint + if version.rpc <= 5 and value > MAX_TEMP_IN_C then + value = st_utils.f_to_c(value) + end + + -- Gather cached setpoint values when considering setpoint limits + -- Note: cached values should always exist, but defaults are chosen just in case to prevent + -- nil operation errors, and deadband logic from triggering. + local cached_cooling_val, cooling_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatCoolingSetpoint.ID, + capabilities.thermostatCoolingSetpoint.coolingSetpoint.NAME, + MAX_TEMP_IN_C, { value = MAX_TEMP_IN_C, unit = "C" } + ) + if cooling_setpoint and cooling_setpoint.unit == "F" then + cached_cooling_val = st_utils.f_to_c(cached_cooling_val) + end + local cached_heating_val, heating_setpoint = device:get_latest_state( + cmd.component, capabilities.thermostatHeatingSetpoint.ID, + capabilities.thermostatHeatingSetpoint.heatingSetpoint.NAME, + MIN_TEMP_IN_C, { value = MIN_TEMP_IN_C, unit = "C" } + ) + if heating_setpoint and heating_setpoint.unit == "F" then + cached_heating_val = st_utils.f_to_c(cached_heating_val) + end + local is_auto_capable = #device:get_endpoints( + clusters.Thermostat.ID, + {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE} + ) > 0 + + --Check setpoint limits for the device + local setpoint_type = string.match(setpoint.NAME, "Heat") or "Cool" + local deadband = device:get_field(fields.setpoint_limit_device_field.MIN_DEADBAND) or 2.5 --spec default + if setpoint_type == "Heat" then + local min = device:get_field(fields.setpoint_limit_device_field.MIN_HEAT) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_HEAT) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + if is_auto_capable and value > (cached_cooling_val - deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is greater than the cooling setpoint (%s) with the deadband (%s)", + value, cooling_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatHeatingSetpoint.heatingSetpoint(heating_setpoint, {state_change = true})) + return + end + else + local min = device:get_field(fields.setpoint_limit_device_field.MIN_COOL) or MIN_TEMP_IN_C + local max = device:get_field(fields.setpoint_limit_device_field.MAX_COOL) or MAX_TEMP_IN_C + if value < min or value > max then + log.warn(string.format( + "Invalid setpoint (%s) outside the min (%s) and the max (%s)", + value, min, max + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + if is_auto_capable and value < (cached_heating_val + deadband) then + log.warn(string.format( + "Invalid setpoint (%s) is less than the heating setpoint (%s) with the deadband (%s)", + value, heating_setpoint, deadband + )) + device:emit_event_for_endpoint(endpoint_id, capabilities.thermostatCoolingSetpoint.coolingSetpoint(cooling_setpoint, {state_change = true})) + return + end + end + device:send(setpoint:write(device, thermostat_utils.component_to_endpoint(device, cmd.component, clusters.Thermostat.ID), st_utils.round(value * 100.0))) + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua new file mode 100644 index 0000000000..102b6e8d3b --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -0,0 +1,324 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local version = require "version" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" +end + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" +end + +if version.api < 13 then + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" +end + +local DeviceConfigurationHelpers = {} + +function DeviceConfigurationHelpers.supported_level_measurements(device) + local measurement_caps, level_caps = {}, {} + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + table.insert(level_caps, cap_id) + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + table.insert(measurement_caps, cap_id) + end + end + end + return measurement_caps, level_caps +end + +function DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + + local supported_thermostat_capabilities = {} + + if #heat_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(supported_thermostat_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(supported_thermostat_capabilities, capabilities.thermostatOperatingState.ID) + end + + return supported_thermostat_capabilities +end + +function DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + local supported_air_quality_capabilities = {} + + local measurement_caps, level_caps = DeviceConfigurationHelpers.supported_level_measurements(device) + + for _, cap_id in ipairs(measurement_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + for _, cap_id in ipairs(level_caps) do + table.insert(supported_air_quality_capabilities, cap_id) + end + + return supported_air_quality_capabilities +end + + +local DeviceConfiguration = {} + +function DeviceConfiguration.match_modular_profile_air_purifer(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local hepa_filter_component_capabilities = {} + local ac_filter_component_capabilties = {} + local profile_name = "air-purifier-modular" + + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + if #temp_eps > 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + + if #hepa_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID, {feature_bitmap = clusters.HepaFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(hepa_filter_component_capabilities, capabilities.filterState.ID) + end + + table.insert(hepa_filter_component_capabilities, capabilities.filterStatus.ID) + end + if #ac_filter_eps > 0 then + local filter_state_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID, {feature_bitmap = clusters.ActivatedCarbonFilterMonitoring.types.Feature.CONDITION}) + if #filter_state_eps > 0 then + table.insert(ac_filter_component_capabilties, capabilities.filterState.ID) + end + + table.insert(ac_filter_component_capabilties, capabilities.filterStatus.ID) + end + + -- determine fan capabilities, note that airPurifierFanMode is already mandatory + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + + if #thermostat_eps > 0 then + -- thermostatMode and temperatureMeasurement should be expected if thermostat is present + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + + -- only add temperatureMeasurement if it is not already added via TemperatureMeasurement cluster support + if #temp_eps == 0 then + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + end + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + end + + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + if #aqs_eps > 0 then + table.insert(main_component_capabilities, capabilities.airQualityHealthConcern.ID) + end + + local supported_air_quality_capabilities = DeviceConfigurationHelpers.get_air_quality_optional_capabilities(device) + for _, capability_id in pairs(supported_air_quality_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + if #ac_filter_component_capabilties > 0 then + table.insert(optional_supported_component_capabilities, {"activatedCarbonFilter", ac_filter_component_capabilties}) + end + if #hepa_filter_component_capabilities > 0 then + table.insert(optional_supported_component_capabilities, {"hepaFilter", hepa_filter_component_capabilities}) + end + + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airPurifierFanMode.ID) + table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.fanSpeedPercent.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) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_thermostat(device) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "thermostat-modular" + + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + + if #fan_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + if #rock_eps > 0 then + table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local thermostat_capabilities = DeviceConfigurationHelpers.get_thermostat_optional_capabilities(device) + for _, capability_id in pairs(thermostat_capabilities) do + table.insert(main_component_capabilities, capability_id) + end + + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_supported == fields.battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif battery_supported == fields.battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +function DeviceConfiguration.match_modular_profile_room_ac(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + local profile_name = "room-air-conditioner-modular" + + if #humidity_eps > 0 then + table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + end + + -- determine fan capabilities + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + -- Note: Room AC does not support the rocking feature of FanControl. + + if #fan_eps > 0 then + table.insert(main_component_capabilities, capabilities.airConditionerFanMode.ID) + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + end + if #wind_eps > 0 then + table.insert(main_component_capabilities, capabilities.windMode.ID) + end + + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + if #heat_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatHeatingSetpoint.ID) + end + if #cool_eps > 0 then + table.insert(main_component_capabilities, capabilities.thermostatCoolingSetpoint.ID) + end + + if running_state_supported then + table.insert(main_component_capabilities, capabilities.thermostatOperatingState.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + + -- 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 + -- add mandatory capabilities for subscription + local total_supported_capabilities = optional_supported_component_capabilities + table.insert(main_component_capabilities, capabilities.switch.ID) + table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + table.insert(main_component_capabilities, capabilities.thermostatMode.ID) + table.insert(main_component_capabilities, capabilities.refresh.ID) + table.insert(main_component_capabilities, capabilities.firmwareUpdate.ID) + + device:set_field(fields.SUPPORTED_COMPONENT_CAPABILITIES, total_supported_capabilities, { persist = true }) + end +end + +local match_modular_device_type = { + [fields.AP_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_air_purifer, + [fields.RAC_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_room_ac, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = DeviceConfiguration.match_modular_profile_thermostat, +} + +local function profiling_data_still_required(device) + for _, field in pairs(fields.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 + +function DeviceConfiguration.match_profile(device) + if profiling_data_still_required(device) then return end + local primary_device_type = thermostat_utils.get_device_type(device) + if version.api >= 14 and version.rpc >= 8 and match_modular_device_type[primary_device_type] then + match_modular_device_type[primary_device_type](device) + return + else + local legacy_device_cfg = require "thermostat_utils.legacy_device_configuration" + legacy_device_cfg.match_profile(device) + end +end + +return DeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua similarity index 67% rename from drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua rename to drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua index 3c38ef55d0..3368fe2fa4 100644 --- a/drivers/SmartThings/matter-thermostat/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/embedded_cluster_utils.lua @@ -1,31 +1,32 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local version = require "version" local clusters = require "st.matter.clusters" local utils = require "st.utils" --- Include driver-side definitions when lua libs api version is < 10 -local version = require "version" if version.api < 10 then - clusters.HepaFilterMonitoring = require "HepaFilterMonitoring" - clusters.ActivatedCarbonFilterMonitoring = require "ActivatedCarbonFilterMonitoring" - clusters.AirQuality = require "AirQuality" - clusters.CarbonMonoxideConcentrationMeasurement = require "CarbonMonoxideConcentrationMeasurement" - clusters.CarbonDioxideConcentrationMeasurement = require "CarbonDioxideConcentrationMeasurement" - clusters.FormaldehydeConcentrationMeasurement = require "FormaldehydeConcentrationMeasurement" - clusters.NitrogenDioxideConcentrationMeasurement = require "NitrogenDioxideConcentrationMeasurement" - clusters.OzoneConcentrationMeasurement = require "OzoneConcentrationMeasurement" - clusters.Pm1ConcentrationMeasurement = require "Pm1ConcentrationMeasurement" - clusters.Pm10ConcentrationMeasurement = require "Pm10ConcentrationMeasurement" - clusters.Pm25ConcentrationMeasurement = require "Pm25ConcentrationMeasurement" - clusters.RadonConcentrationMeasurement = require "RadonConcentrationMeasurement" - clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "ElectricalPowerMeasurement" + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" end if version.api < 13 then - clusters.WaterHeaterMode = require "WaterHeaterMode" + clusters.WaterHeaterMode = require "embedded_clusters.WaterHeaterMode" end local embedded_cluster_utils = {} diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua new file mode 100644 index 0000000000..770fd7d65e --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua @@ -0,0 +1,238 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local st_utils = require "st.utils" + +if version.api < 10 then + clusters.CarbonDioxideConcentrationMeasurement = require "embedded_clusters.CarbonDioxideConcentrationMeasurement" + clusters.CarbonMonoxideConcentrationMeasurement = require "embedded_clusters.CarbonMonoxideConcentrationMeasurement" + clusters.Pm10ConcentrationMeasurement = require "embedded_clusters.Pm10ConcentrationMeasurement" + clusters.Pm25ConcentrationMeasurement = require "embedded_clusters.Pm25ConcentrationMeasurement" + clusters.FormaldehydeConcentrationMeasurement = require "embedded_clusters.FormaldehydeConcentrationMeasurement" + clusters.NitrogenDioxideConcentrationMeasurement = require "embedded_clusters.NitrogenDioxideConcentrationMeasurement" + clusters.OzoneConcentrationMeasurement = require "embedded_clusters.OzoneConcentrationMeasurement" + clusters.RadonConcentrationMeasurement = require "embedded_clusters.RadonConcentrationMeasurement" + clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" + clusters.Pm1ConcentrationMeasurement = require "embedded_clusters.Pm1ConcentrationMeasurement" + clusters.Thermostat.types.ThermostatSystemMode.DRY = 0x8 -- ThermostatSystemMode added in Matter 1.2 + clusters.Thermostat.types.ThermostatSystemMode.SLEEP = 0x9 -- ThermostatSystemMode added in Matter 1.2 +end + +local ThermostatFields = {} + +ThermostatFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" + +ThermostatFields.SAVED_SYSTEM_MODE_IB = "__saved_system_mode_ib" +ThermostatFields.DISALLOWED_THERMOSTAT_MODES = "__DISALLOWED_CONTROL_OPERATIONS" +ThermostatFields.OPTIONAL_THERMOSTAT_MODES_SEEN = "__OPTIONAL_THERMOSTAT_MODES_SEEN" + +ThermostatFields.RAC_DEVICE_TYPE_ID = 0x0072 +ThermostatFields.AP_DEVICE_TYPE_ID = 0x002D +ThermostatFields.FAN_DEVICE_TYPE_ID = 0x002B +ThermostatFields.WATER_HEATER_DEVICE_TYPE_ID = 0x050F +ThermostatFields.HEAT_PUMP_DEVICE_TYPE_ID = 0x0309 +ThermostatFields.THERMOSTAT_DEVICE_TYPE_ID = 0x0301 +ThermostatFields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID = 0x0510 + +ThermostatFields.MIN_ALLOWED_PERCENT_VALUE = 0 +ThermostatFields.MAX_ALLOWED_PERCENT_VALUE = 100 +ThermostatFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" +ThermostatFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" +ThermostatFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds +ThermostatFields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP = "__total_cumulative_energy_imported_map" +ThermostatFields.SUPPORTED_WATER_HEATER_MODES_WITH_IDX = "__supported_water_heater_modes_with_idx" +ThermostatFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" +ThermostatFields.MGM3_PPM_CONVERSION_FACTOR = 24.45 + +-- For RPC version < 6: +-- issue context: driver cannot know a setpoint capability's unit (whether Celsius or Farenheit) +-- when a command is received, as the received arguments do not contain the unit. +-- workaround: map the following temperature ranges to either Celsius or Farenheit: +-- For Thermostats: +-- 1. if the received setpoint command value is in the range 5 ~ 40, it is inferred as *C +-- 2. if the received setpoint command value is in the range 41 ~ 104, it is inferred as *F +-- For Water Heaters: +-- 1. if the received setpoint command value is in the range 30 ~ 80, it is inferred as *C +-- 2. if the received setpoint command value is in the range 86 ~ 176, it is inferred as *F +-- For RPC version >= 6: +-- temperatureSetpoint always reports in Celsius, removing the need for the above workaround. +-- In this case, we use these fields simply to limit the setpoint's range to "reasonable" values on the platform. +ThermostatFields.THERMOSTAT_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 40.0 +ThermostatFields.THERMOSTAT_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 5.0 +ThermostatFields.WATER_HEATER_MAX_TEMP_IN_C = version.rpc >= 6 and 100.0 or 80.0 +ThermostatFields.WATER_HEATER_MIN_TEMP_IN_C = version.rpc >= 6 and 0.0 or 30.0 + +ThermostatFields.setpoint_limit_device_field = { + MIN_SETPOINT_DEADBAND_CHECKED = "MIN_SETPOINT_DEADBAND_CHECKED", + MIN_HEAT = "MIN_HEAT", + MAX_HEAT = "MAX_HEAT", + MIN_COOL = "MIN_COOL", + MAX_COOL = "MAX_COOL", + MIN_DEADBAND = "MIN_DEADBAND", + MIN_TEMP = "MIN_TEMP", + MAX_TEMP = "MAX_TEMP" +} + +ThermostatFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} + +ThermostatFields.profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", + THERMOSTAT_RUNNING_STATE_SUPPORT = "__THERMOSTAT_RUNNING_STATE_SUPPORT" +} + +ThermostatFields.THERMOSTAT_MODE_MAP = { + [clusters.Thermostat.types.ThermostatSystemMode.OFF] = capabilities.thermostatMode.thermostatMode.off, + [clusters.Thermostat.types.ThermostatSystemMode.AUTO] = capabilities.thermostatMode.thermostatMode.auto, + [clusters.Thermostat.types.ThermostatSystemMode.COOL] = capabilities.thermostatMode.thermostatMode.cool, + [clusters.Thermostat.types.ThermostatSystemMode.HEAT] = capabilities.thermostatMode.thermostatMode.heat, + [clusters.Thermostat.types.ThermostatSystemMode.EMERGENCY_HEATING] = capabilities.thermostatMode.thermostatMode.emergency_heat, + [clusters.Thermostat.types.ThermostatSystemMode.PRECOOLING] = capabilities.thermostatMode.thermostatMode.precooling, + [clusters.Thermostat.types.ThermostatSystemMode.FAN_ONLY] = capabilities.thermostatMode.thermostatMode.fanonly, + [clusters.Thermostat.types.ThermostatSystemMode.DRY] = capabilities.thermostatMode.thermostatMode.dryair, + [clusters.Thermostat.types.ThermostatSystemMode.SLEEP] = capabilities.thermostatMode.thermostatMode.asleep, +} + +ThermostatFields.THERMOSTAT_OPERATING_MODE_MAP = { + [0] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [1] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [2] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [3] = capabilities.thermostatOperatingState.thermostatOperatingState.heating, + [4] = capabilities.thermostatOperatingState.thermostatOperatingState.cooling, + [5] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, + [6] = capabilities.thermostatOperatingState.thermostatOperatingState.fan_only, +} + +ThermostatFields.WIND_MODE_MAP = { + [0] = capabilities.windMode.windMode.sleepWind, + [1] = capabilities.windMode.windMode.naturalWind +} + +ThermostatFields.ROCK_MODE_MAP = { + [0] = capabilities.fanOscillationMode.fanOscillationMode.horizontal, + [1] = capabilities.fanOscillationMode.fanOscillationMode.vertical, + [2] = capabilities.fanOscillationMode.fanOscillationMode.swing +} + +ThermostatFields.AIR_QUALITY_MAP = { + {capabilities.carbonDioxideMeasurement.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonDioxideHealthConcern.ID, "-co2", clusters.CarbonDioxideConcentrationMeasurement}, + {capabilities.carbonMonoxideMeasurement.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.carbonMonoxideHealthConcern.ID, "-co", clusters.CarbonMonoxideConcentrationMeasurement}, + {capabilities.dustSensor.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.dustHealthConcern.ID, "-pm10", clusters.Pm10ConcentrationMeasurement}, + {capabilities.fineDustSensor.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.fineDustHealthConcern.ID, "-pm25", clusters.Pm25ConcentrationMeasurement}, + {capabilities.formaldehydeMeasurement.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.formaldehydeHealthConcern.ID, "-ch2o", clusters.FormaldehydeConcentrationMeasurement}, + {capabilities.nitrogenDioxideHealthConcern.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.nitrogenDioxideMeasurement.ID, "-no2", clusters.NitrogenDioxideConcentrationMeasurement}, + {capabilities.ozoneHealthConcern.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.ozoneMeasurement.ID, "-ozone", clusters.OzoneConcentrationMeasurement}, + {capabilities.radonHealthConcern.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.radonMeasurement.ID, "-radon", clusters.RadonConcentrationMeasurement}, + {capabilities.tvocHealthConcern.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.tvocMeasurement.ID, "-tvoc", clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement}, + {capabilities.veryFineDustHealthConcern.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, + {capabilities.veryFineDustSensor.ID, "-pm1", clusters.Pm1ConcentrationMeasurement}, +} + +ThermostatFields.units = { + PPM = 0, + PPB = 1, + PPT = 2, + MGM3 = 3, + UGM3 = 4, + NGM3 = 5, + PM3 = 6, + BQM3 = 7, + PCIL = 0xFF -- not in matter spec +} + +local units = ThermostatFields.units -- copy units to avoid references below + +ThermostatFields.unit_strings = { + [units.PPM] = "ppm", + [units.PPB] = "ppb", + [units.PPT] = "ppt", + [units.MGM3] = "mg/m^3", + [units.NGM3] = "ng/m^3", + [units.UGM3] = "μg/m^3", + [units.BQM3] = "Bq/m^3", + [units.PCIL] = "pCi/L" +} + +ThermostatFields.unit_default = { + [capabilities.carbonMonoxideMeasurement.NAME] = units.PPM, + [capabilities.carbonDioxideMeasurement.NAME] = units.PPM, + [capabilities.nitrogenDioxideMeasurement.NAME] = units.PPM, + [capabilities.ozoneMeasurement.NAME] = units.PPM, + [capabilities.formaldehydeMeasurement.NAME] = units.PPM, + [capabilities.veryFineDustSensor.NAME] = units.UGM3, + [capabilities.fineDustSensor.NAME] = units.UGM3, + [capabilities.dustSensor.NAME] = units.UGM3, + [capabilities.radonMeasurement.NAME] = units.BQM3, + [capabilities.tvocMeasurement.NAME] = units.PPB -- TVOC is typically within the range of 0-5500 ppb, with good to moderate values being < 660 ppb +} + +-- All ConcentrationMesurement clusters inherit from the same base cluster definitions, +-- so CarbonMonoxideConcentratinMeasurement is used below but the same enum types exist +-- in all ConcentrationMeasurement clusters +ThermostatFields.level_strings = { + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.UNKNOWN] = "unknown", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.LOW] = "good", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.MEDIUM] = "moderate", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.HIGH] = "unhealthy", + [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", +} + +-- measured in g/mol +ThermostatFields.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", +} + +ThermostatFields.conversion_tables = { + [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.PPB] = { + [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 st_utils.round(value/(10^6)) end + }, + [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.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.NGM3] = { + [units.UGM3] = function(value) return st_utils.round(value/(10^3)) end + }, + [units.BQM3] = { + [units.PCIL] = function(value) return st_utils.round(value/37) end + }, +} + +return ThermostatFields \ No newline at end of file diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua new file mode 100644 index 0000000000..6cdda2a53a --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/legacy_device_configuration.lua @@ -0,0 +1,259 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local version = require "version" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local fields = require "thermostat_utils.fields" +local thermostat_utils = require "thermostat_utils.utils" + +if version.api < 10 then + clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" + clusters.ActivatedCarbonFilterMonitoring = require "embedded_clusters.ActivatedCarbonFilterMonitoring" + clusters.AirQuality = require "embedded_clusters.AirQuality" +end + +local LegacyConfigurationHelpers = {} + +function LegacyConfigurationHelpers.create_level_measurement_profile(device) + local meas_name, level_name = "", "" + for _, details in ipairs(fields.AIR_QUALITY_MAP) do + local cap_id = details[1] + local cluster = details[3] + -- capability describes either a HealthConcern or Measurement/Sensor + if (cap_id:match("HealthConcern$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) + if #attr_eps > 0 then + level_name = level_name .. details[2] + end + elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then + local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) + if #attr_eps > 0 then + meas_name = meas_name .. details[2] + end + end + end + return meas_name, level_name +end + +function LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + local aqs_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) + local profile_name = "" + if #aqs_eps > 0 then + profile_name = profile_name .. "-aqs" + end + local meas_name, level_name = LegacyConfigurationHelpers.create_level_measurement_profile(device) + if meas_name ~= "" then + profile_name = profile_name .. meas_name .. "-meas" + end + if level_name ~= "" then + profile_name = profile_name .. level_name .. "-level" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_fan_profile(device) + local fan_eps = device:get_endpoints(clusters.FanControl.ID) + local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) + local rock_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.ROCKING}) + local profile_name = "" + if #fan_eps > 0 then + profile_name = profile_name .. "-fan" + end + if #rock_eps > 0 then + profile_name = profile_name .. "-rock" + end + if #wind_eps > 0 then + profile_name = profile_name .. "-wind" + end + return profile_name +end + +function LegacyConfigurationHelpers.create_air_purifier_profile(device) + local hepa_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.HepaFilterMonitoring.ID) + local ac_filter_eps = embedded_cluster_utils.get_endpoints(device, clusters.ActivatedCarbonFilterMonitoring.ID) + local fan_eps_seen = false + local profile_name = "air-purifier" + if #hepa_filter_eps > 0 then + profile_name = profile_name .. "-hepa" + end + if #ac_filter_eps > 0 then + profile_name = profile_name .. "-ac" + end + + -- air purifier profiles include -fan later in the name for historical reasons. + -- save this information for use at that point. + local fan_profile = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_profile ~= "" then + fan_eps_seen = true + end + fan_profile = string.gsub(fan_profile, "-fan", "") + profile_name = profile_name .. fan_profile + + return profile_name, fan_eps_seen +end + +function LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + local heat_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) + local cool_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) + + local thermostat_modes = "" + if #heat_eps == 0 and #cool_eps == 0 then + return "No Heating nor Cooling Support" + elseif #heat_eps > 0 and #cool_eps == 0 then + thermostat_modes = thermostat_modes .. "-heating-only" + elseif #cool_eps > 0 and #heat_eps == 0 then + thermostat_modes = thermostat_modes .. "-cooling-only" + end + return thermostat_modes +end + + +local LegacyDeviceConfiguration = {} + +function LegacyDeviceConfiguration.match_profile(device) + local running_state_supported = device:get_field(fields.profiling_data.THERMOSTAT_RUNNING_STATE_SUPPORT) + local battery_supported = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + + local thermostat_eps = device:get_endpoints(clusters.Thermostat.ID) + local humidity_eps = device:get_endpoints(clusters.RelativeHumidityMeasurement.ID) + local device_type = thermostat_utils.get_device_type(device) + local profile_name + if device_type == fields.RAC_DEVICE_TYPE_ID then + profile_name = "room-air-conditioner" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- Room AC does not support the rocking feature of FanControl. + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + fan_name = string.gsub(fan_name, "-rock", "") + profile_name = profile_name .. fan_name + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "" then + profile_name = profile_name .. "-heating-cooling" + else + device.log.warn_with({hub_logs=true}, "Device does not support both heating and cooling. No matching profile") + return + end + + if profile_name == "room-air-conditioner-humidity-fan-wind-heating-cooling" then + profile_name = "room-air-conditioner" + end + + if not running_state_supported and profile_name == "room-air-conditioner-fan-heating-cooling" then + profile_name = profile_name .. "-nostate" + end + + elseif device_type == fields.FAN_DEVICE_TYPE_ID then + profile_name = LegacyConfigurationHelpers.create_fan_profile(device) + -- remove leading "-" + profile_name = string.sub(profile_name, 2) + if profile_name == "fan" then + profile_name = "fan-generic" + end + + elseif device_type == fields.AP_DEVICE_TYPE_ID then + local fan_eps_found + profile_name, fan_eps_found = LegacyConfigurationHelpers.create_air_purifier_profile(device) + if #thermostat_eps > 0 then + profile_name = profile_name .. "-thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes ~= "No Heating nor Cooling Support" then + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + elseif #device:get_endpoints(clusters.TemperatureMeasurement.ID) > 0 then + profile_name = profile_name .. "-temperature" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + if fan_eps_found then + profile_name = profile_name .. "-fan" + end + end + profile_name = profile_name .. LegacyConfigurationHelpers.create_air_quality_sensor_profile(device) + elseif device_type == fields.WATER_HEATER_DEVICE_TYPE_ID then + -- If a Water Heater is composed of Electrical Sensor device type, it must support both ElectricalEnergyMeasurement and + -- ElectricalPowerMeasurement clusters. + local electrical_sensor_eps = thermostat_utils.get_endpoints_by_device_type(device, fields.ELECTRICAL_SENSOR_DEVICE_TYPE_ID) or {} + if #electrical_sensor_eps > 0 then + profile_name = "water-heater-power-energy-powerConsumption" + end + elseif device_type == fields.HEAT_PUMP_DEVICE_TYPE_ID then + profile_name = "heat-pump" + local MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS = 2 + for i = 1, math.min(MAX_HEAT_PUMP_THERMOSTAT_COMPONENTS, #thermostat_eps) do + profile_name = profile_name .. "-thermostat" + if thermostat_utils.tbl_contains(humidity_eps, thermostat_eps[i]) then + profile_name = profile_name .. "-humidity" + end + end + elseif #thermostat_eps > 0 then + profile_name = "thermostat" + + if #humidity_eps > 0 then + profile_name = profile_name .. "-humidity" + end + + -- thermostat profiles support neither wind nor rocking FanControl attributes + local fan_name = LegacyConfigurationHelpers.create_fan_profile(device) + if fan_name ~= "" then + profile_name = profile_name .. "-fan" + end + + local thermostat_modes = LegacyConfigurationHelpers.create_thermostat_modes_profile(device) + if thermostat_modes == "No Heating nor Cooling Support" then + device.log.warn_with({hub_logs=true}, "Device does not support either heating or cooling. No matching profile") + return + else + profile_name = profile_name .. thermostat_modes + end + + if not running_state_supported then + profile_name = profile_name .. "-nostate" + end + + if battery_supported == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif battery_supported == fields.battery_support.NO_BATTERY then + profile_name = profile_name .. "-nobattery" + end + else + device.log.warn_with({hub_logs=true}, "Device type is not supported in thermostat driver") + return + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end + -- clear all profiling data fields after profiling is complete. + for _, field in pairs(fields.profiling_data) do + device:set_field(field, nil) + end +end + +return LegacyDeviceConfiguration diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua new file mode 100644 index 0000000000..8194f08c48 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua @@ -0,0 +1,183 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local log = require "log" +local capabilities = require "st.capabilities" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local fields = require "thermostat_utils.fields" + +local ThermostatUtils = {} + +function ThermostatUtils.tbl_contains(array, value) + if value == nil then return false end + for _, element in pairs(array or {}) do + if element == value then + return true + end + end + return false +end + +function ThermostatUtils.get_field_for_endpoint(device, field, endpoint) + return device:get_field(string.format("%s_%d", field, endpoint)) +end + +function ThermostatUtils.set_field_for_endpoint(device, field, endpoint, value, additional_params) + device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) +end + +function ThermostatUtils.find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + 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 + 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 + +function ThermostatUtils.component_to_endpoint(device, component_name, cluster_id) + -- Use the find_default_endpoint function to return the first endpoint that + -- supports a given cluster. + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil and component_to_endpoint_map[component_name] ~= nil then + return component_to_endpoint_map[component_name] + end + if not cluster_id then return device.MATTER_DEFAULT_ENDPOINT end + return ThermostatUtils.find_default_endpoint(device, cluster_id) +end + +function ThermostatUtils.endpoint_to_component(device, endpoint_id) + local component_to_endpoint_map = device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) + if component_to_endpoint_map ~= nil then + for comp, ep in pairs(component_to_endpoint_map) do + if ep == endpoint_id then + return comp + end + end + end + return "main" +end + +function ThermostatUtils.get_total_cumulative_energy_imported(device) + local total_cumulative_energy_imported = device:get_field(fields.TOTAL_CUMULATIVE_ENERGY_IMPORTED_MAP) or {} + local total_energy = 0 + for _, energyWh in pairs(total_cumulative_energy_imported) do + total_energy = total_energy + energyWh + end + return total_energy +end + +function ThermostatUtils.get_endpoints_by_device_type(device, device_type) + local endpoints = {} + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == device_type then + table.insert(endpoints, ep.endpoint_id) + break + end + end + end + table.sort(endpoints) + return endpoints +end + +function ThermostatUtils.get_device_type(device) + -- For cases where a device has multiple device types, this list indicates which + -- device type will be the "main" device type for purposes of selecting a profile + -- with an appropriate category. This is done to promote consistency between + -- devices with similar device type compositions that may report their device types + -- listed in different orders + local device_type_priority = { + [fields.HEAT_PUMP_DEVICE_TYPE_ID] = 1, + [fields.RAC_DEVICE_TYPE_ID] = 2, + [fields.AP_DEVICE_TYPE_ID] = 3, + [fields.THERMOSTAT_DEVICE_TYPE_ID] = 4, + [fields.FAN_DEVICE_TYPE_ID] = 5, + [fields.WATER_HEATER_DEVICE_TYPE_ID] = 6, + } + + local main_device_type = false + + for _, ep in ipairs(device.endpoints) do + if ep.device_types ~= nil then + for _, dt in ipairs(ep.device_types) do + if not device_type_priority[main_device_type] or (device_type_priority[dt.device_type_id] and + device_type_priority[dt.device_type_id] < device_type_priority[main_device_type]) then + main_device_type = dt.device_type_id + end + end + end + end + + 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 + 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") + return + end + + return conversion_function(value, fields.molecular_weights[capability_name]) +end + +function ThermostatUtils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + +function ThermostatUtils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) + local current_time = os.time() + local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 + + -- Ensure that the previous report was sent at least 15 minutes ago + if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then + return + end + + device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) + + -- Calculate the energy delta between reports + local energy_delta_wh = 0.0 + local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME) + if previous_imported_report and previous_imported_report.energy then + energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) + end + + local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end + + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' + device:emit_component_event(device.profile.components["main"], capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = energy_delta_wh, + energy = latest_total_imported_energy_wh + })) +end + +return ThermostatUtils From 7355666729403bf40cbe051b3f9c5086b01af8f2 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Tue, 25 Nov 2025 10:55:07 -0600 Subject: [PATCH 283/449] WWSTCERT-9021 Aqara Presence Multi-Sensor FP300 (update profile) --- drivers/SmartThings/matter-sensor/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 36ec8b6bc8..3aec6b5df9 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -14,7 +14,7 @@ matterManufacturer: deviceLabel: Presence Multi-Sensor FP300 vendorId: 0x115F productId: 0x2005 - deviceProfileName: motion-illuminance-temperature-humidity-batteryLevel + deviceProfileName: presence-illuminance-temperature-humidity-battery #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" From 5b9ff3af1be94067fc9d547fdded2b032abe7fe1 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:22:48 -0600 Subject: [PATCH 284/449] deprecate matter- prefixed profiles (#2583) --- .../matter-sensor/profiles/matter-motion-battery-illuminance.yml | 1 + .../SmartThings/matter-sensor/profiles/matter-motion-battery.yml | 1 + .../profiles/matter-motion-batteryLevel-illuminance.yml | 1 + .../matter-sensor/profiles/matter-motion-batteryLevel.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml index b6c30d3421..3bac085178 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml index 0e2b57c71e..66ea49c033 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-battery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-battery components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml index 87c7fdf701..f9f97b3360 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel-illuminance.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel-illuminance components: - id: main diff --git a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml index afc752557f..fa497ba042 100644 --- a/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml +++ b/drivers/SmartThings/matter-sensor/profiles/matter-motion-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: matter-motion-batteryLevel components: - id: main From fb88d26353165d112e7f40df0030497c8ed618db Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 25 Nov 2025 14:37:37 -0800 Subject: [PATCH 285/449] WWSTCERT-9117 OSRAM MATTER PLUG UK WWSTCERT-9120 WWSTCERT-9123 WWSTCERT-9126 WWSTCERT-9129 WWSTCERT-9132 WWSTCERT-9135 --- .../matter-switch/fingerprints.yml | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index c67eec7bf2..0bc24ec7ca 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -905,6 +905,41 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0AC3 deviceProfileName: light-color-level + - id: "4489/61444" + deviceLabel: OSRAM MATTER PLUG UK + vendorId: 0x1189 + productId: 0xF004 + deviceProfileName: plug-binary + - id: "4489/61443" + deviceLabel: OSRAM MATTER PLUG EU WH + vendorId: 0x1189 + productId: 0xF003 + deviceProfileName: plug-binary + - id: "4489/61441" + deviceLabel: OSRAM MATTER CLASSIC A 60W + vendorId: 0x1189 + productId: 0xF001 + deviceProfileName: light-color-level + - id: "4489/2353" + deviceLabel: SMART MATTER FLOORCORN200 MGC WT + vendorId: 0x1189 + productId: 0x0931 + deviceProfileName: light-color-level + - id: "4489/2350" + deviceLabel: SMART MATTER FLOORCORN140 MGC BK + vendorId: 0x1189 + productId: 0x092E + deviceProfileName: light-color-level + - id: "4489/2352" + deviceLabel: SMART MATTER FLOORCORN140 MGC WT + vendorId: 0x1189 + productId: 0x0930 + deviceProfileName: light-color-level + - id: "4489/2351" + deviceLabel: SMART MATTER FLOORCORN200 MGC BK + vendorId: 0x1189 + productId: 0x092F + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 5aeb76d928fa22e14d56a9523e5f40be47a5dca1 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 25 Nov 2025 14:40:22 -0800 Subject: [PATCH 286/449] WWSTCERT-9113 LUX TQX Smart Thermostat --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 8358e3164e..3ece3ead62 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -44,6 +44,11 @@ matterManufacturer: vendorId: 0x1206 productId: 0x0001 deviceProfileName: thermostat-nostate-nobattery + - id: "4614/17" + deviceLabel: LUX TQX Smart Thermostat + vendorId: 0x1206 + productId: 0x0011 + deviceProfileName: thermostat-humidity-nostate-nobattery #Meross - id: "4933/57345" deviceLabel: Smart Wi-Fi Thermostat From b69e1d518e0493d7ba7704c80538f3f82f5535f3 Mon Sep 17 00:00:00 2001 From: Inovelli <37669481+InovelliUSA@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:41:18 -0700 Subject: [PATCH 287/449] =?UTF-8?q?WWSTCERT-8802=20Inovelli:=20adding=20su?= =?UTF-8?q?pport=20for=20vzm30=20(zigbee=20on/off)=20and=20adding=20suppor?= =?UTF-8?q?ted=20button=20events=20to=20all=20de=E2=80=A6=20(#2537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adding support for vzm30 and adding supported button events to all devices * removing some preferences. update test unit * fix linter errors for test files * better naming for drivers * updating supportedValues for devices that may have been included with the old driver * fixing linter errors * fixing linter errors * fixing linter errors --- .../zigbee-switch/fingerprints.yml | 5 + .../profiles/inovelli-vzm30-sn.yml | 231 ++++++++ .../SmartThings/zigbee-switch/src/init.lua | 2 + .../zigbee-switch/src/inovelli/can_handle.lua | 1 + .../zigbee-switch/src/inovelli/common.lua | 41 +- .../zigbee-switch/src/inovelli/init.lua | 81 ++- .../src/inovelli/sub_drivers.lua | 1 + .../src/inovelli/vzm30-sn/can_handle.lua | 16 + .../src/inovelli/vzm30-sn/init.lua | 53 ++ .../src/inovelli/vzm32-sn/init.lua | 2 +- .../src/test/test_inovelli_vzm30_sn.lua | 537 ++++++++++++++++++ .../src/test/test_inovelli_vzm30_sn_child.lua | 356 ++++++++++++ .../test_inovelli_vzm30_sn_preferences.lua | 210 +++++++ .../src/test/test_inovelli_vzm31_sn.lua | 188 ++++-- .../src/test/test_inovelli_vzm32_sn.lua | 197 +++++-- 15 files changed, 1779 insertions(+), 142 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index d5f2e4d8df..3dfa9970b5 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2359,6 +2359,11 @@ zigbeeManufacturer: model: NEXENTRO Dimming Actuator deviceProfileName: on-off-level # Inovelli + - id: "Inovelli/VZM30-SN" + deviceLabel: "Inovelli On/Off Blue Series" + manufacturer: Inovelli + model: VZM30-SN + deviceProfileName: inovelli-vzm30-sn - id: "Inovelli/VZM31-SN" deviceLabel: "Inovelli 2-in-1 Blue Series" manufacturer: Inovelli diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml new file mode 100644 index 0000000000..ff670c0097 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm30-sn.yml @@ -0,0 +1,231 @@ +name: inovelli-vzm30-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: temperatureMeasurement + version: 1 + - id: relativeHumidityMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: Switch + - id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter258" + title: "258. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: false + preferenceType: enumeration + definition: + options: + "0": "Dimmer" + "1": "On/Off (default)" + default: 1 + - name: "parameter22" + title: "22. Aux Switch Type" + description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "None" + "1": "3-Way Aux Switch (default)" + default: 1 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 126 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=0" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 0 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter11" + title: "11. Invert Switch" + description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." + required: false + preferenceType: enumeration + definition: + options: + "0": "No (default)" + "1": "Yes" + default: 0 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 696ff8ada9..1b694d2a5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -66,6 +66,8 @@ local zigbee_switch_driver_template = { capabilities.energyMeter, capabilities.motionSensor, capabilities.illuminanceMeasurement, + capabilities.relativeHumidityMeasurement, + capabilities.temperatureMeasurement, }, sub_drivers = { lazy_load_if_possible("non_zigbee_devices"), diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua index 473c1cb807..d9b676e307 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/can_handle.lua @@ -4,6 +4,7 @@ return function(opts, driver, device) local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, { mfr = "Inovelli", model = "VZM31-SN" }, { mfr = "Inovelli", model = "VZM32-SN" } } diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua index 33d3ff577c..c402731328 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -4,9 +4,27 @@ local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local zigbee_constants = require "st.zigbee.constants" +local capabilities = require "st.capabilities" local M = {} +M.supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +-- Utility function to check if device is VZM32-SN +function M.is_vzm32(device) + return device:get_model() == "VZM32-SN" +end + +-- Utility function to check if device is VZM32-SN +function M.is_vzm30(device) + return device:get_model() == "VZM30-SN" +end + -- Sends a generic configure for Inovelli devices (all models): -- - device:configure -- - send OTA ImageNotify @@ -25,10 +43,31 @@ function M.base_device_configure(driver, device, private_cluster_id, mfg_code) device:send(device_management.build_bind_request(device, private_cluster_id, driver.environment_info.hub_zigbee_eui, 2)) -- Read divisors/multipliers for power/energy reporting - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + -- Set default divisor to 1000 for VZM32-SN and VZM30-SN. In initial firmware the divisor is incorrectly set to 100. + if M.is_vzm32(device) or M.is_vzm30(device) then + device:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + else + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + end device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + + for _, component in pairs(device.profile.components) do + if component.id ~= "main" then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + M.supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end end return M \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 73967b41f3..a83a343663 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -30,8 +30,6 @@ local base_preference_map = { parameter2 = {parameter_number = 2, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter3 = {parameter_number = 3, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter4 = {parameter_number = 4, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, - parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter15 = {parameter_number = 15, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter95 = {parameter_number = 95, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter96 = {parameter_number = 96, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, @@ -41,12 +39,20 @@ local base_preference_map = { -- Model-specific overrides/additions local model_preference_overrides = { + ["VZM30-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + }, ["VZM31-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, }, ["VZM32-SN"] = { + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter34 = {parameter_number = 34, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, parameter101 = {parameter_number = 101, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, parameter102 = {parameter_number = 102, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, @@ -126,9 +132,55 @@ local function scene_handler(driver, device, zb_rx) local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] local additional_fields = { state_change = true } local event = capability_attribute and capability_attribute(additional_fields) or nil - local comp = device.profile.components[button_to_component(button_number)] + local comp_name = button_to_component(button_number) + local comp = device.profile.components[comp_name] if comp ~= nil and event ~= nil then - device:emit_component_event(comp, event) + -- Check if the event is in the supportedButtonValues before emitting + -- This ensures backward compatibility with devices installed with previous driver versions + local expected_values = inovelli_common.supported_button_values[comp_name] + local supportedEvents = device:get_latest_state( + comp_name, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME, + {capabilities.button.button.pushed.NAME} -- default fallback for older devices + ) + + -- Check if supportedButtonValues needs to be updated + -- This handles devices installed with previous driver versions that don't have + -- the updated supportedButtonValues attribute. If the current value only contains + -- "pushed" (the fallback), update it to the full list. + local needs_update = false + if expected_values then + -- Check if current supportedEvents is exactly the fallback (only "pushed") + -- This indicates the state was never set and we're using the fallback value + if #supportedEvents == 1 and supportedEvents[1] == capabilities.button.button.pushed.NAME then + needs_update = true + end + + if needs_update then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + expected_values, + { visibility = { displayed = false } } + ) + ) + supportedEvents = expected_values -- Update local reference for event check + end + end + + -- Check if the event is supported + local event_supported = false + for _, event_name in pairs(supportedEvents) do + if event.value.value == event_name then + event_supported = true + break + end + end + + if event_supported then + device:emit_component_event(comp, event) + end end end @@ -200,18 +252,6 @@ local function device_configure(driver, device) end end -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - local function huePercentToValue(value) if value <= 2 then return 0 elseif value >= 98 then return 255 @@ -328,7 +368,7 @@ local function handle_resetEnergyMeter(self, device) end local inovelli = { - NAME = "inovelli combined handler", + NAME = "Inovelli Zigbee Switch", lifecycle_handlers = { doConfigure = device_configure, infoChanged = info_changed, @@ -336,13 +376,6 @@ local inovelli = { }, zigbee_handlers = { attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - }, [OccupancySensing.ID] = { [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler }, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua index 20677e4764..2fc65221be 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/sub_drivers.lua @@ -4,5 +4,6 @@ local lazy_load = require "lazy_load_subdriver" return { + lazy_load("inovelli.vzm30-sn"), lazy_load("inovelli.vzm32-sn") } diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua new file mode 100644 index 0000000000..9d1b285b45 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(opts, driver, device) + local INOVELLI_VZM30_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM30-SN" }, + } + for _, fp in ipairs(INOVELLI_VZM30_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local sub_driver = require("inovelli.vzm30-sn") + return true, sub_driver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua new file mode 100644 index 0000000000..6ad3ec758d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm30-sn/init.lua @@ -0,0 +1,53 @@ +-- Copyright 2025 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 clusters = require "st.zigbee.zcl.clusters" +local st_device = require "st.device" +local inovelli_common = require "inovelli.common" + +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +local function configure_temperature_reporting(device) + local min_temp_change = 50 -- 0.5°C in 0.01°C units + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_temp_change)) +end + +local function configure_humidity_reporting(device) + local min_humidity_change = 50 -- 0.5% in 0.01% units + device:send(RelativeHumidity.attributes.MeasuredValue:configure_reporting(device, 30, 3600, min_humidity_change)) +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + configure_temperature_reporting(device) + configure_humidity_reporting(device) + else + device:configure() + end +end + +local vzm30_sn = { + NAME = "Inovelli VZM30-SN Zigbee Switch", + can_handle = require("inovelli.vzm30-sn.can_handle"), + lifecycle_handlers = { + doConfigure = device_configure, + }, +} + +return vzm30_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua index edadf85b90..4e0151aeb6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -52,7 +52,7 @@ local function device_configure(driver, device) end local vzm32_sn = { - NAME = "inovelli vzm32-sn device-specific", + NAME = "Inovelli VZM32-SN mmWave Dimmer", can_handle = require("inovelli.vzm32-sn.can_handle"), lifecycle_handlers = { added = device_added, 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 new file mode 100644 index 0000000000..79e7a66b54 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -0,0 +1,537 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" + +local OnOff = clusters.OnOff +local Level = clusters.Level +local TemperatureMeasurement = clusters.TemperatureMeasurement +local RelativeHumidity = clusters.RelativeHumidity + +-- Inovelli VZM30-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM30_SN_MODEL = "VZM30-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM30_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300, 0x0402, 0x0405} -- OnOff, Level, ColorControl, TemperatureMeasurement, RelativeHumidity + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} + } + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should send read commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:read(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm30_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm30_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm30_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event and update supportedButtonValues", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event and update supportedButtonValues", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm30_sn.id, build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + build_inovelli_button_message(mock_inovelli_vzm30_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + +-- Test temperature measurement +test.register_message_test( + "Temperature measurement should emit temperature events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 2500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 25.0, unit = "C"})) + } + } +) + +-- Test humidity measurement +test.register_message_test( + "Humidity measurement should emit humidity events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + RelativeHumidity.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm30_sn, 6500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity(65)) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_coroutine_test( + "Power meter from ElectricalMeasurement should emit power events", + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm30_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm30_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end +) + +-- Test energy meter +test.register_coroutine_test( + "Energy meter should emit energy events", + function() + -- Set the divisor field as the device does during configuration + -- For VZM30-SN, the divisor is set to 1000 (like VZM32-SN) + mock_inovelli_vzm30_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm30_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm30_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm30_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm30_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm30_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.register_coroutine_test( + "doConfigure runs base + VZM30 extras", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm30_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm30_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm30_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm30_sn, 1, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, RelativeHumidity.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 100) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm30_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm30_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, TemperatureMeasurement.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 600, 100) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm30_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, device_management.build_bind_request(mock_inovelli_vzm30_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm30_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm30_sn) }) + + -- VZM30-specific: temperature and humidity reporting configuration + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm30_sn.id, RelativeHumidity.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm30_sn, 30, 3600, 50) }) + + mock_inovelli_vzm30_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..e40ead721a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua @@ -0,0 +1,356 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..0726a2d575 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua @@ -0,0 +1,210 @@ +-- Copyright 2025 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 test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm30_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM30-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm30_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm30-sn.yml"), + zigbee_endpoints = inovelli_vzm30_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM30-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm30_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter15 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter15 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 15, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter95 preference should send configuration command", + function() + local new_param_value = 64 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter95 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 95, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM30-only, same as VZM31) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm30_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm30_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 22, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm30_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM30-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm30_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +test.run_registered_tests() 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 d70159dde5..a3d57415b1 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 @@ -10,6 +10,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" -- Inovelli VZM31-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" @@ -38,6 +39,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -181,24 +188,38 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", @@ -208,69 +229,96 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) - } - } -) - --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm31_sn, 1500) - } + message = mock_inovelli_vzm31_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) }, { channel = "capability", direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + mock_inovelli_vzm31_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm31_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device reads during configuration + -- For VZM31, the divisor is read from the device, but for testing we need to set it + -- The test expects 50000 -> 500.0 kWh, which means divisor of 100 + mock_inovelli_vzm31_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 100, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -325,21 +373,49 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base config (VZM31)", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm31_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm31_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) test.run_registered_tests() - 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 debdeba110..0a288f27d4 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 @@ -10,6 +10,7 @@ local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade local device_management = require "st.zigbee.device_management" +local zigbee_constants = require "st.zigbee.constants" local OnOff = clusters.OnOff local Level = clusters.Level @@ -41,6 +42,12 @@ local function test_init() end test.set_test_init_function(test_init) +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + -- Test device initialization test.register_message_test( "Device should initialize properly on added lifecycle event", @@ -225,38 +232,101 @@ end -- Test button1 pushed test.register_message_test( - "Button1 pushed should emit button event", + "Button1 pushed should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) -- Test button2 pressed 4 times test.register_message_test( - "Button2 pressed 4 times should emit button event", + "Button2 pressed 4 times should emit button event and update supportedButtonValues", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x02, 0x05) } }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message( + "button2", + capabilities.button.supportedButtonValues( + supported_button_values["button2"], + { visibility = { displayed = false } } + ) + ) + }, { channel = "capability", direction = "send", message = mock_inovelli_vzm32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } + }, + { + inner_block_ordering = "relaxed" } ) +-- Test consecutive button events - supportedButtonValues should only be sent on first event +test.register_coroutine_test( + "Consecutive button events should only send supportedButtonValues on first event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + + -- First button event: button1 pushed - should send supportedButtonValues + button event + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + "button1", + capabilities.button.supportedButtonValues( + supported_button_values["button1"], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + + -- Second button event: button1 pushed_2x - should only send button event, NOT supportedButtonValues + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x03) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) + ) + end +) + -- Test illuminance measurement test.register_message_test( "Illuminance measurement should emit illuminance events", @@ -317,64 +387,41 @@ test.register_message_test( } ) --- Test power meter from SimpleMetering -test.register_message_test( - "Power meter from SimpleMetering should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm32_sn, 1500) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) - } - } -) - -- Test power meter from ElectricalMeasurement -test.register_message_test( +test.register_coroutine_test( "Power meter from ElectricalMeasurement should emit power events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) - } - } + function() + -- Set the divisor field (default handlers use 10 if not set, but we can set it for consistency) + -- The default handler will use 10 if ELECTRICAL_MEASUREMENT_DIVISOR_KEY is not set + -- Since the test expects 2000 -> 200.0 W, that means divisor of 10 is being used + -- For VZM32, the actual device reads ACPowerDivisor, but default is 10 + mock_inovelli_vzm32_sn:set_field(zigbee_constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + ) + end ) -- Test energy meter -test.register_message_test( +test.register_coroutine_test( "Energy meter should emit energy events", - { - { - channel = "zigbee", - direction = "receive", - message = { - mock_inovelli_vzm32_sn.id, - clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 50000) - } - }, - { - channel = "capability", - direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) - } - } + function() + -- Set the divisor field as the device does during configuration + mock_inovelli_vzm32_sn:set_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY, 1000, {persist = true}) + + test.socket.zigbee:__queue_receive({ + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 212) + }) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 0.212, unit = "kWh"})) + ) + end ) -- Test energy meter reset command @@ -429,22 +476,52 @@ test.register_message_test( test.register_coroutine_test( "doConfigure runs base + VZM32 extras", function() + test.socket.capability:__set_channel_ordering("relaxed") test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) + + -- Button capability messages from base_device_configure + for _, component in pairs(mock_inovelli_vzm32_sn.profile.components) do + if component.id ~= "main" then + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzm32_sn:generate_test_message( + component.id, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + ) + end + end + + -- device:configure() sends bind requests and configure reporting (default handler) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- base_device_configure sends OTA ImageNotify and private cluster bind test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm32_sn) }) + + -- Read divisors/multipliers test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) - test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + + -- VZM32-specific: occupancy and illuminance reporting configuration test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file From c0f12f8d20b188f5ef65820e7bdab1805745a8c6 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 26 Nov 2025 10:44:18 -0800 Subject: [PATCH 288/449] WWSTCERT-9105 Meross Smart Presence Sensor (Thread) (#2578) * WWSTCERT-9105 Meross Smart Presence Sensor (Thread) * use non-deprecated profile * use correct profile name --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3aec6b5df9..8836c276cd 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -128,6 +128,11 @@ matterManufacturer: vendorId: 0x1345 productId: 0x4201 deviceProfileName: motion-illuminance + - id: "4933/16898" + deviceLabel: Smart Presence Sensor (Thread) + vendorId: 0x1345 + productId: 0x4202 + deviceProfileName: motion-illuminance-battery # Neo - id: "4991/1122" deviceLabel: Door Sensor From 37691d50b13b618f7ce058743a5ce9c9972d873d Mon Sep 17 00:00:00 2001 From: Inovelli <37669481+InovelliUSA@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:44:19 -0700 Subject: [PATCH 289/449] WWSTCERT-8360 Inovelli VZW32-SN: Adding support for this device for the WWST program (#2371) * Add support for Inovelli mmWave switch * Fix for measurement unit being cm (not mm) * Configure illuminance reporting and fix p101-106 unit incorrect * adjusting lux reporting. remove p117 as mmwave param and updated its options * initializing values for occupancy, illuminance, and binding to occupancy cluster * add ability to reset energy meter * add ability to reset energy meter * adjusting some default parameters and adding ota image notify for firmware update process during certification * adding missing OTAUpgrade declaration * Inovelli - adding support for vzw32-sn for wwst * accidentally included some vzm32-sn (zigbee) changes in this branch. Removing them * some updates from linter results. Notification turns on when color changed. Update the app UI * Removing unsupported capabilities * fixing mmwave reset command * remove some preferences. Prevent extra events being sent for child device during init * add copyright info * remove firmware update capability * remove unused configuration handler and notification class * removing on/off and level handlers * removing version report features * add unit tests * updating notification profile as requested by ST * update unit test for different rgbw profile * remove versionget command * fix lua errors * some more fixes for test unit files * missed an unused variable * modify unit test files. Changes to device init to be more efficient * Remove unused code * linter fixes * remove unused init function * fixing linter error * removing firmware update from rgbw bulb. Adding custom refresh handler * modify refresh command so it doesn't kill the driver * consolidating drivers. Updating some unit tests * Fixing some linter warnings * fix inovelli button test error * fix linter warning on button test * fix color init for lzw31 black switch * better naming of driver files * will updated supportedValues for held and down_hold events. This can likely be removed in the future * linter warnings fixed * linter warnings fixed * fix minor formatting issues and copyright issue --------- Co-authored-by: InovelliUSA --- .../SmartThings/zwave-switch/fingerprints.yml | 10 +- .../profiles/inovelli-dimmer-power-energy.yml | 24 + .../inovelli-mmwave-dimmer-vzw32-sn.yml | 400 ++++++++++++++ .../zwave-switch/profiles/rgbw-bulb.yml | 16 + drivers/SmartThings/zwave-switch/src/init.lua | 2 +- .../src/inovelli-LED/can_handle.lua | 21 - .../zwave-switch/src/inovelli-LED/init.lua | 82 --- .../inovelli-LED/inovelli-lzw31sn/init.lua | 87 --- .../zwave-switch/src/inovelli/can_handle.lua | 20 + .../zwave-switch/src/inovelli/init.lua | 501 ++++++++++++++++++ .../lzw31-sn}/can_handle.lua | 6 +- .../src/inovelli/lzw31-sn/init.lua | 74 +++ .../sub_drivers.lua | 3 +- .../src/inovelli/vzw32-sn/can_handle.lua | 19 + .../src/inovelli/vzw32-sn/init.lua | 73 +++ .../zwave-switch/src/preferences.lua | 38 ++ .../src/test/test_inovelli_button.lua | 49 +- .../src/test/test_inovelli_dimmer_scenes.lua | 20 +- .../src/test/test_inovelli_vzw32_sn.lua | 325 ++++++++++++ .../src/test/test_inovelli_vzw32_sn_child.lua | 335 ++++++++++++ .../test_inovelli_vzw32_sn_preferences.lua | 151 ++++++ 21 files changed, 2043 insertions(+), 213 deletions(-) create mode 100644 drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml create mode 100644 drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml delete mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua delete mode 100644 drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli/init.lua rename drivers/SmartThings/zwave-switch/src/{inovelli-LED/inovelli-lzw31sn => inovelli/lzw31-sn}/can_handle.lua (69%) create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua rename drivers/SmartThings/zwave-switch/src/{inovelli-LED => inovelli}/sub_drivers.lua (67%) create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua create mode 100644 drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua create mode 100644 drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua create mode 100644 drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua create mode 100644 drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index 7154dc11c7..d90a0c6b76 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -45,17 +45,23 @@ zwaveManufacturer: productId: 0x0000 deviceProfileName: switch-level - id: "Inovelli/Dimmer/Power/Energy" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Red Series manufacturerId: 0x031E productType: 0x0001 productId: 0x0001 deviceProfileName: inovelli-dimmer-power-energy - id: "Inovelli/Dimmer" - deviceLabel: Inovelli Dimmer Switch + deviceLabel: Inovelli Dimmer Black Series manufacturerId: 0x031E productType: 0x0003 productId: 0x0001 deviceProfileName: inovelli-dimmer + - id: "Inovelli/VZW32-SN" + deviceLabel: Inovelli mmWave Dimmer Red Series + manufacturerId: 0x031E + productType: 0x0017 + productId: 0x0001 + deviceProfileName: inovelli-mmwave-dimmer-vzw32-sn - id: "010F/0403" deviceLabel: Fibaro Single Switch manufacturerId: 0x010F diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml index c3ee571c10..077c80e3a3 100644 --- a/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-dimmer-power-energy.yml @@ -27,24 +27,48 @@ components: categories: - name: Light - id: button1 + label: Down Button capabilities: - id: button version: 1 categories: - name: RemoteController - id: button2 + label: Up Button capabilities: - id: button version: 1 categories: - name: RemoteController - id: button3 + label: Config Button capabilities: - id: button version: 1 categories: - name: RemoteController preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "0": "Clear" + "1": "Solid" + "2": "Chase" + "3": "Fast Blink" + "4": "Slow Blink" + "5": "Pulse" + default: 1 - name: "dimmingSpeed" title: "Dimming Speed" description: "How fast or slow the light changes state when you hold the switch. diff --git a/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml new file mode 100644 index 0000000000..d85a661839 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/inovelli-mmwave-dimmer-vzw32-sn.yml @@ -0,0 +1,400 @@ +name: inovelli-mmwave-dimmer-vzw32-sn +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +- id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +- id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +- id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter158" + title: "158. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: true + preferenceType: enumeration + definition: + options: + "0": "Dimmer (default)" + "1": "On/Off" + default: 0 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=255 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 255 + default: 255 + - name: "parameter9" + title: "9. Minimum Level" + description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." + required: true + preferenceType: number + definition: + minimum: 1 + maximum: 99 + default: 1 + - name: "parameter10" + title: "10. Maximum Level" + description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." + required: true + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter18" + title: "18. Active Power Reports" + description: "Power level change that will result in a new power report being sent. + 0 = Disabled + 1-32767 = 0.1W-3276.7W." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter19" + title: "19. Periodic Power & Energy Reports" + description: "Time period between consecutive power & energy reports being sent (in seconds). The timer is reset after each report is sent." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 3600 + - name: "parameter20" + title: "20. Active Energy Reports" + description: "Energy level change that will result in a new energy report being sent. + 0 = Disabled + 1-32767 = 0.01kWh-327.67kWh." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 32767 + default: 100 + - name: "parameter50" + title: "50. Button Press Delay" + description: "Adjust the delay used in scene control. 0=no delay (disables multi-tap scenes), 1=100ms, 2=200ms, 3=300ms, etc." + required: true + preferenceType: enumeration + definition: + options: + "0": "0ms" + "1": "100ms" + "2": "200ms" + "3": "300ms" + "4": "400ms" + "5": "500ms (default)" + "6": "600ms" + "7": "700ms" + "8": "800ms" + "9": "900ms" + default: 5 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: true + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: true + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in cm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in cm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter108" + title: "108. mmWave Stay Life" + description: "Optimize detection in areas where user may be still for a long time. The delay time of the stay area is set to 50ms when it is set to 1, to 1 second when it is set to 20, and the default value is 300, that is, 15 seconds" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 300 + - name: "parameter110" + title: "Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "0": "Factory Reset Module" + default: 3 + - name: "parameter112" + title: "112. mmWave Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "mmWave Detection Timeout" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 4294967296 + default: 30 \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml new file mode 100644 index 0000000000..d87d32adc6 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/profiles/rgbw-bulb.yml @@ -0,0 +1,16 @@ +name: rgbw-bulb +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: colorTemperature + version: 1 + - id: colorControl + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 5814945884..405600e962 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -122,7 +122,7 @@ local driver_template = { }, sub_drivers = { lazy_load_if_possible("eaton-accessory-dimmer"), - lazy_load_if_possible("inovelli-LED"), + lazy_load_if_possible("inovelli"), lazy_load_if_possible("dawon-smart-plug"), lazy_load_if_possible("inovelli-2-channel-smart-plug"), lazy_load_if_possible("zwave-dual-switch"), diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua deleted file mode 100644 index eed5127205..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/can_handle.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local INOVELLI_MANUFACTURER_ID = 0x031E -local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 -local INOVELLI_LZW31_PRODUCT_TYPE = 0x0003 -local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 - -local function can_handle_inovelli_led(opts, driver, device, ...) - if device:id_match( - INOVELLI_MANUFACTURER_ID, - {INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_LZW31_PRODUCT_TYPE}, - INOVELLI_DIMMER_PRODUCT_ID - ) then - local subdriver = require("inovelli-LED") - return true, subdriver - end - return false -end - -return can_handle_inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua deleted file mode 100644 index 23fafda05d..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/init.lua +++ /dev/null @@ -1,82 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" ---- @type st.utils -local utils = require "st.utils" ---- @type st.zwave.constants -local constants = require "st.zwave.constants" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" ---- @type st.zwave.CommandClass.Configuration -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) - -local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 -local LED_GENERIC_SATURATION = 100 -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local function huePercentToZwaveValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function zwaveValueToHuePercent(value) - if value <= 2 then - return 0 - elseif value >= 254 then - return 100 - else - return utils.round(value / 255 * 100) - end -end - -local function configuration_report(driver, device, cmd) - if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER then - local hue = zwaveValueToHuePercent(cmd.args.configuration_value) - - local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] - if ledBarComponent ~= nil then - device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) - device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) - end - end -end - -local function set_color(driver, device, cmd) - local value = huePercentToZwaveValue(cmd.args.color.hue) - local config = Configuration:Set({ - parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, - configuration_value=value, - size=2 - }) - device:send(config) - - local query_configuration = function() - device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) - end - - device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) -end - -local inovelli_led = { - NAME = "Inovelli LED", - zwave_handlers = { - [cc.CONFIGURATION] = { - [Configuration.REPORT] = configuration_report - } - }, - capability_handlers = { - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - } - }, - can_handle = require("inovelli-LED.can_handle"), - sub_drivers = require("inovelli-LED.sub_drivers"), -} - -return inovelli_led diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua deleted file mode 100644 index fd0ff5d844..0000000000 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/init.lua +++ /dev/null @@ -1,87 +0,0 @@ --- Copyright 2025 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.CentralScene -local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) - -local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" - -local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button3"] = {"pushed"} -} - -local function device_added(driver, device) - for _, component in pairs(device.profile.components) do - if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - supported_button_values[component.id], - { visibility = { displayed = false } } - ) - ) - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - device:refresh() -end - -local map_scene_number_to_component = { - [1] = "button2", - [2] = "button1", - [3] = "button3" -} - - -local map_key_attribute_to_capability = { - [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, - [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, - [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, - [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, - [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, -} - -local function central_scene_notification_handler(self, device, cmd) - if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then - local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - if event ~= nil then - -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 - local comp = device.profile.components[map_scene_number_to_component[cmd.args.scene_number]] - if comp ~= nil then - device:emit_component_event(comp, event) - end - end - end -end - -local inovelli_led_lzw31sn = { - NAME = "Inovelli LED LZW 31SN", - zwave_handlers = { - [cc.CENTRAL_SCENE] = { - [CentralScene.NOTIFICATION] = central_scene_notification_handler - } - }, - lifecycle_handlers = { - added = device_added - }, - can_handle = require("inovelli-LED.inovelli-lzw31sn.can_handle") -} - -return inovelli_led_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua new file mode 100644 index 0000000000..7c0f6be77b --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_FINGERPRINTS = { + { mfr = 0x031E, prod = 0x0017, model = 0x0001 }, -- Inovelli VZW32-SN + { mfr = 0x031E, prod = 0x0001, model = 0x0001 }, -- Inovelli LZW31SN + { mfr = 0x031E, prod = 0x0003, model = 0x0001 }, -- Inovelli LZW31 +} + +local function can_handle_inovelli(opts, driver, device, ...) + for _, fingerprint in ipairs(INOVELLI_FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end + +return can_handle_inovelli diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua new file mode 100644 index 0000000000..84ed2c7df7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/init.lua @@ -0,0 +1,501 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.Configuration +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchBinary +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) +--- @type st.zwave.CommandClass.Basic +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +local preferencesMap = require "preferences" + +--- @type st.utils +local utils = require "st.utils" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +local log = require "log" +local st_device = require "st.device" + +--- @type st.zwave.CommandClass.CentralScene +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +--- @type st.zwave.constants +local constants = require "st.zwave.constants" + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local GEN3_NOTIFICATION_PARAMETER_NUMBER = 99 +local GEN2_NOTIFICATION_PARAMETER_NUMBER = 16 +local LED_COLOR_CONTROL_PARAMETER_NUMBER = 13 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" +local LED_GENERIC_SATURATION = 100 + +-- TODO: Remove after transition period - supportedButtonValues initialization +-- This table defines the supported button values for each button component. +-- Used to initialize supportedButtonValues on device_added and update devices with old values. +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed"} +} + +-- Device type detection helpers +local function is_gen2(device) + return device:id_match(0x031E, {0x0001, 0x0003}, 0x0001) +end + +local function is_gen3(device) + return device:id_match(0x031E, {0x0015, 0x0017}, 0x0001) +end + +-- Helper function to get the correct notification parameter number based on device type +local function get_notification_parameter_number(device) + -- For child devices, check the parent device type + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + if is_gen3(device_to_check) then + return GEN3_NOTIFICATION_PARAMETER_NUMBER + else + return GEN2_NOTIFICATION_PARAMETER_NUMBER + end +end + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return utils.round(value / 100 * 255) + end +end + +local function valueToHuePercent(value) + if value <= 2 then + return 0 + elseif value >= 254 then + return 100 + else + return utils.round(value / 255 * 100) + end +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then -- in case the value is boolean + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if type == 4 and new_value > 2147483647 then + return ((4294967296 - new_value) * -1) + elseif type == 2 and new_value > 32767 then + return ((65536 - new_value) * -1) + elseif type == 1 and new_value > 127 then + return ((256 - new_value) * -1) + else + return new_value + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function getNotificationValue(device, value) + 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 duration = 255 -- Default duration + + -- Get the parent device to check generation for child devices + local device_to_check = device + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device_to_check = device:get_parent_device() + end + + local colorValue = huePercentToValue(value or color) + local notificationValue = 0 + + if is_gen3(device_to_check) then + -- Gen3 order: duration, level, color, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (colorValue * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (duration * 1) -- byte 0 (lowest) + else + -- Gen2 order: color, level, duration, effect (bytes 0-3 from low to high) + notificationValue = notificationValue + (effect * 16777216) -- byte 3 (highest) + notificationValue = notificationValue + (duration * 65536) -- byte 2 + notificationValue = notificationValue + (level * 256) -- byte 1 + notificationValue = notificationValue + (colorValue * 1) -- byte 0 (lowest) + end + + return notificationValue +end + +local function set_color(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(command.args.color.hue) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function set_color_temperature(driver, device, command) + if device.network_type == st_device.NETWORK_TYPE_CHILD then + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device, 100), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + else + local value = huePercentToValue(100) + local config = Configuration:Set({ + parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER, + configuration_value=value, + size=2 + }) + device:send(config) + + local query_configuration = function() + device:send(Configuration:Get({ parameter_number=LED_COLOR_CONTROL_PARAMETER_NUMBER })) + end + + device.thread:call_with_delay(constants.DEFAULT_GET_STATUS_DELAY, query_configuration) + end +end + +local function switch_level_set(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local level = utils.round(command.args.level) + level = utils.clamp_value(level, 0, 99) + + device:send(SwitchMultilevel:Set({ value=level, duration=command.args.rate or "default" })) + + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + 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() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=getNotificationValue(device), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function refresh_handler(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + refresh_handler(driver, device) + if is_gen2(device) then + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then + time_diff = os.difftime(os.time(), last_clock_set_time) + end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = preferencesMap.get_device_parameters(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb','notification') + end + end + + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + device:send(Configuration:Set({parameter_number = preferences[id].parameter_number, size = preferences[id].size, configuration_value = new_parameter_value})) + end + end + else + log.info("info_changed running more than once. Cancelling this run. Time diff: " .. time_diff) + end + end +end + +local function switch_set_on_off_handler(value) + return function(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Basic:Set({ value = value })) + device.thread:call_with_delay(3, function(d) + device:send(SwitchMultilevel:Get({})) + end) + else + device:emit_event(capabilities.switch.switch(value == 0 and "off" or "on")) + local dev = device:get_parent_device() + local config = Configuration:Set({ + parameter_number=get_notification_parameter_number(device), + configuration_value=(value == 0 and 0 or getNotificationValue(device)), + size=4 + }) + local send_configuration = function() + dev:send(config) + end + device.thread:call_with_delay(1,send_configuration) + end + end +end + +local function configuration_report(driver, device, cmd) + if cmd.args.parameter_number == LED_COLOR_CONTROL_PARAMETER_NUMBER and is_gen2(device) then + local hue = valueToHuePercent(cmd.args.configuration_value) + + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(hue)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(LED_GENERIC_SATURATION)) + end + end +end + +local map_key_attribute_to_capability = { + [CentralScene.key_attributes.KEY_PRESSED_1_TIME] = capabilities.button.button.pushed, + [CentralScene.key_attributes.KEY_RELEASED] = capabilities.button.button.held, + [CentralScene.key_attributes.KEY_HELD_DOWN] = capabilities.button.button.down_hold, + [CentralScene.key_attributes.KEY_PRESSED_2_TIMES] = capabilities.button.button.pushed_2x, + [CentralScene.key_attributes.KEY_PRESSED_3_TIMES] = capabilities.button.button.pushed_3x, + [CentralScene.key_attributes.KEY_PRESSED_4_TIMES] = capabilities.button.button.pushed_4x, + [CentralScene.key_attributes.KEY_PRESSED_5_TIMES] = capabilities.button.button.pushed_5x, +} + +-- Map key attributes to their button value strings for support checking +-- TODO: This mapping and the support check below can likely be removed after a transition period. +-- Once users have interacted with their devices and the supportedButtonValues gets properly +-- set during device initialization, the driver will know which values are supported and +-- won't attempt to emit unsupported events. This code is a temporary safeguard to prevent +-- errors during the transition period. +local map_key_attribute_to_value = { + [CentralScene.key_attributes.KEY_RELEASED] = "held", + [CentralScene.key_attributes.KEY_HELD_DOWN] = "down_hold", +} + +-- TODO: Remove after transition period - button value support checking +-- Helper function to check if a button value is supported. +-- This function can likely be removed after a transition period once devices have +-- their supportedButtonValues properly set. See comment above map_key_attribute_to_value. +local function is_button_value_supported(device, component, value) + if value == nil then + return true -- If no value to check, assume supported + end + + local supported_values_state = device:get_latest_state( + component.id, + capabilities.button.ID, + capabilities.button.supportedButtonValues.NAME + ) + + -- Check multiple possible structures for supportedButtonValues + -- In SmartThings Edge, get_latest_state returns a state object + -- For supportedButtonValues, the array could be in: state.value, or state itself IS the array + local supported_values = nil + if supported_values_state ~= nil then + -- First check .value property (most common structure) + if supported_values_state.value ~= nil then + supported_values = supported_values_state.value + -- Check if state itself is an array (the state IS the array) + -- Check if index 1 exists - if it does and .value doesn't exist, the state itself is the array + elseif type(supported_values_state) == "table" and supported_values_state[1] ~= nil then + supported_values = supported_values_state + end + + -- Check .state.value structure (nested structure) + if supported_values == nil and supported_values_state.state ~= nil and supported_values_state.state.value ~= nil then + supported_values = supported_values_state.state.value + end + end + + if supported_values == nil then + return true -- If no supported values set, assume all are supported (backward compatibility) + end + + -- Check if the value is in the supported values array + if type(supported_values) == "table" then + for _, supported_value in ipairs(supported_values) do + if supported_value == value then + return true + end + end + end + + return false +end + +local function central_scene_notification_handler(self, device, cmd) + if ( cmd.args.scene_number ~= nil and cmd.args.scene_number ~= 0 ) then + local button_number = cmd.args.scene_number + local capability_attribute = map_key_attribute_to_capability[cmd.args.key_attributes] + local additional_fields = { + state_change = true + } + + local event + if capability_attribute ~= nil then + event = capability_attribute(additional_fields) + end + + if event ~= nil then + -- device reports scene notifications from endpoint 0 (main) but central scene events have to be emitted for button components: 1,2,3 + local component_name = button_to_component(button_number) + local comp = device.profile.components[component_name] + if comp ~= nil then + -- TODO: Remove after transition period - button value support checking + -- Check if held or down_hold are supported before emitting. + -- This support check can likely be removed after a transition period once devices + -- have their supportedButtonValues properly set. The driver will then only emit events + -- for values that are actually supported, preventing errors. See comment above map_key_attribute_to_value. + local button_value = map_key_attribute_to_value[cmd.args.key_attributes] + local is_supported = is_button_value_supported(device, comp, button_value) + if button_value == nil or is_supported then + device:emit_component_event(comp, event) + else + -- TODO: Remove after transition period - supportedButtonValues update for old devices + -- Update supportedButtonValues for devices with old values from previous driver versions. + -- After updating, emit the event since the value is now supported. + if supported_button_values[comp.id] ~= nil then + device:emit_component_event( + comp, + capabilities.button.supportedButtonValues( + supported_button_values[comp.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event(comp, event) + end + end + end + end + end +end + +------------------------------------------------------------------------------------------- +-- Register message handlers and run driver +------------------------------------------------------------------------------------------- +local inovelli = { + NAME = "Inovelli Z-Wave Switch", + lifecycle_handlers = { + infoChanged = info_changed, + added = device_added, + }, + zwave_handlers = { + [cc.CENTRAL_SCENE] = { + [CentralScene.NOTIFICATION] = central_scene_notification_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.switch.on.NAME] = switch_set_on_off_handler(SwitchBinary.value.ON_ENABLE), + [capabilities.switch.switch.off.NAME] = switch_set_on_off_handler(SwitchBinary.value.OFF_DISABLE) + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_set + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.can_handle"), + sub_drivers = require("inovelli.sub_drivers"), +} + +return inovelli \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua similarity index 69% rename from drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua rename to drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua index 611a8881c2..6b70eb3c27 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/inovelli-lzw31sn/can_handle.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/can_handle.lua @@ -5,15 +5,15 @@ local INOVELLI_MANUFACTURER_ID = 0x031E local INOVELLI_LZW31SN_PRODUCT_TYPE = 0x0001 local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 -local function can_handle_inovelli_lzw31sn(opts, driver, device, ...) +local function can_handle_lzw31sn(opts, driver, device, ...) if device:id_match( INOVELLI_MANUFACTURER_ID, INOVELLI_LZW31SN_PRODUCT_TYPE, INOVELLI_DIMMER_PRODUCT_ID ) then - return true, require("inovelli-LED.inovelli-lzw31sn") + return true, require("inovelli.lzw31-sn") end return false end -return can_handle_inovelli_lzw31sn +return can_handle_lzw31sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua new file mode 100644 index 0000000000..77a5430681 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/lzw31-sn/init.lua @@ -0,0 +1,74 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed"} +} + +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= LED_BAR_COMPONENT_NAME then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end + refresh_handler(driver, device) + local ledBarComponent = device.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + device:emit_component_event(ledBarComponent, capabilities.colorControl.hue(1)) + device:emit_component_event(ledBarComponent, capabilities.colorControl.saturation(1)) + end + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local lzw31_sn = { + NAME = "Inovelli LZW31-SN Z-Wave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.lzw31-sn.can_handle") +} + +return lzw31_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua similarity index 67% rename from drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua rename to drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua index 6a51cd350b..e182120ece 100644 --- a/drivers/SmartThings/zwave-switch/src/inovelli-LED/sub_drivers.lua +++ b/drivers/SmartThings/zwave-switch/src/inovelli/sub_drivers.lua @@ -4,5 +4,6 @@ local lazy_load = require "lazy_load_subdriver" return { - lazy_load("inovelli-LED.inovelli-lzw31sn"), + lazy_load("inovelli.lzw31-sn"), + lazy_load("inovelli.vzw32-sn") } diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua new file mode 100644 index 0000000000..5ed4e272e0 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_DIMMER_PRODUCT_ID = 0x0001 + +local function can_handle_vzw32_sn(opts, driver, device, ...) + if device:id_match( + INOVELLI_MANUFACTURER_ID, + INOVELLI_VZW32_SN_PRODUCT_TYPE, + INOVELLI_DIMMER_PRODUCT_ID + ) then + return true, require("inovelli.vzw32-sn") + end + return false +end + +return can_handle_vzw32_sn diff --git a/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua new file mode 100644 index 0000000000..0ad1d31430 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/inovelli/vzw32-sn/init.lua @@ -0,0 +1,73 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass.SwitchMultilevel +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +--- @type st.zwave.CommandClass.SensorMultilevel +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 7 }) +--- @type st.zwave.CommandClass.Meter +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.Association +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +--- @type st.device +local st_device = require "st.device" + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +local function refresh_handler(driver, device) + device:send(SwitchMultilevel:Get({})) + device:send(SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE})) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.WATTS })) + device:send(Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS })) + device:send(Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION})) +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(Association:Set({grouping_identifier = 1, node_ids = {driver.environment_info.hub_zwave_id}})) + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= "LEDColorConfiguration" then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + supported_button_values[component.id], + { visibility = { displayed = false } } + ) + ) + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end + refresh_handler(driver, device) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local vzw32_sn = { + NAME = "Inovelli VZW32-SN mmWave Dimmer", + lifecycle_handlers = { + added = device_added, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler + } + }, + can_handle = require("inovelli.vzw32-sn.can_handle") +} + +return vzw32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zwave-switch/src/preferences.lua b/drivers/SmartThings/zwave-switch/src/preferences.lua index af8e8cf772..824e570070 100644 --- a/drivers/SmartThings/zwave-switch/src/preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/preferences.lua @@ -59,6 +59,44 @@ local devices = { switchType = {parameter_number = 22, size = 1} } }, + INOVELLI_VZW32_SN = { + MATCHING_MATRIX = { + mfrs = 0x031E, + product_types = {0x0017}, + product_ids = 0x0001 + }, + PARAMETERS = { + parameter158 = {parameter_number = 158, size = 1}, + parameter52 = {parameter_number = 52, size = 1}, + parameter1 = {parameter_number = 1, size = 1}, + parameter2 = {parameter_number = 2, size = 1}, + parameter3 = {parameter_number = 3, size = 1}, + parameter4 = {parameter_number = 4, size = 1}, + parameter9 = {parameter_number = 9, size = 1}, + parameter10 = {parameter_number = 10, size = 1}, + parameter15 = {parameter_number = 15, size = 1}, + parameter18 = {parameter_number = 18, size = 1}, + parameter19 = {parameter_number = 19, size = 2}, + parameter20 = {parameter_number = 20, size = 2}, + parameter50 = {parameter_number = 50, size = 1}, + parameter95 = {parameter_number = 95, size = 1}, + parameter96 = {parameter_number = 96, size = 1}, + parameter97 = {parameter_number = 97, size = 1}, + parameter98 = {parameter_number = 98, size = 1}, + parameter101 = {parameter_number = 101, size = 2}, + parameter102 = {parameter_number = 102, size = 2}, + parameter103 = {parameter_number = 103, size = 2}, + parameter104 = {parameter_number = 104, size = 2}, + parameter105 = {parameter_number = 105, size = 2}, + parameter106 = {parameter_number = 106, size = 2}, + parameter108 = {parameter_number = 108, size = 4}, + parameter110 = {parameter_number = 110, size = 2}, + parameter111 = {parameter_number = 111, size = 1}, + parameter112 = {parameter_number = 112, size = 1}, + parameter113 = {parameter_number = 113, size = 1}, + parameter114 = {parameter_number = 114, size = 4} + } + }, QUBINO_FLUSH_DIMMER = { MATCHING_MATRIX = { mfrs = 0x0159, 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 4391102104..0863f9eb43 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua @@ -5,8 +5,10 @@ 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 Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Meter = (require "st.zwave.CommandClass.Meter")({ version = 3 }) local t_utils = require "integration_test.utils" local INOVELLI_MANUFACTURER_ID = 0x031E @@ -55,8 +57,8 @@ end test.set_test_init_function(test_init) local supported_button_values = { - ["button1"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, - ["button2"] = {"pushed", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"}, + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, ["button3"] = {"pushed"} } @@ -89,9 +91,44 @@ test.register_coroutine_test( test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_inovelli_dimmer, - Basic:Get({}) + Association:Set({ grouping_identifier = 1, node_ids = {}, payload = "\x01" }) ) ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_dimmer, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + + local ledBarComponent = mock_inovelli_dimmer.profile.components[LED_BAR_COMPONENT_NAME] + if ledBarComponent ~= nil then + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.hue(1) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_dimmer:generate_test_message( + LED_BAR_COMPONENT_NAME, + capabilities.colorControl.saturation(1) + ) + ) + end end ) @@ -115,7 +152,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({state_change = true})) + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({state_change = true})) } } ) @@ -139,7 +176,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } ) 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 1202b187e2..316b9b1254 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 @@ -54,7 +54,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true })) } } @@ -74,7 +74,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -94,7 +94,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -114,7 +114,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -134,7 +134,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ state_change = true })) } } @@ -154,7 +154,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) } } @@ -174,7 +174,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_2x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_2x({ state_change = true })) } } @@ -194,7 +194,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_3x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) } } @@ -214,7 +214,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_4x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_4x({ state_change = true })) } } @@ -234,7 +234,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_inovelli_dimmer:generate_test_message("button2", capabilities.button.button.pushed_5x({ + message = mock_inovelli_dimmer:generate_test_message("button1", capabilities.button.button.pushed_5x({ state_change = true })) } } 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 new file mode 100644 index 0000000000..5593ced043 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua @@ -0,0 +1,325 @@ +-- Copyright 2025 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 SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) +local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local CentralScene = (require "st.zwave.CommandClass.CentralScene")({version=3}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) +local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version=7}) +local Meter = (require "st.zwave.CommandClass.Meter")({version=3}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 +local LED_BAR_COMPONENT_NAME = "LEDColorConfiguration" + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + {value = zw.SENSOR_MULTILEVEL}, + {value = zw.METER}, + {value = zw.NOTIFICATION}, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_sn) +end +test.set_test_init_function(test_init) + +local supported_button_values = { + ["button1"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button2"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + ["button3"] = {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"} +} + +-- Test device initialization +test.register_coroutine_test( + "Device should initialize properly on added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzw32_sn.id, "added" }) + + for button_name, _ in pairs(mock_inovelli_vzw32_sn.profile.components) do + if button_name ~= "main" and button_name ~= LED_BAR_COMPONENT_NAME then + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.supportedButtonValues( + supported_button_values[button_name], + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_inovelli_vzw32_sn:generate_test_message( + button_name, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Association:Set({ + grouping_identifier = 1, + node_ids = {}, -- Mock hub Z-Wave ID + payload = "\x01", -- Should contain grouping_identifier = 1 + }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + ) + end +) + +-- Test switch on command +test.register_coroutine_test( + "Switch on command should send Basic Set with ON value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "on", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.ON_ENABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch off command +test.register_coroutine_test( + "Switch off command should send Basic Set with OFF value", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switch", command = "off", args = {} } + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Basic:Set({ value = SwitchBinary.value.OFF_DISABLE }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(3) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test switch level command +test.register_coroutine_test( + "Switch level command should send SwitchMultilevel Set", + function() + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + + test.socket.capability:__queue_receive({ + mock_inovelli_vzw32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + }) + + local expected_command = SwitchMultilevel:Set({ value = 50, duration = "default" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + expected_command + ) + ) + + test.wait_for_events() + test.mock_time.advance_time(3) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + ) + end +) + +-- Test central scene notifications +test.register_message_test( + "Central scene notification should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 1, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_1_TIME + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button1", capabilities.button.button.pushed({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test central scene notifications - button2 pressed 4 times +test.register_message_test( + "Central scene notification button2 pressed 4 times should emit button events", + { + { + channel = "zwave", + direction = "receive", + message = { mock_inovelli_vzw32_sn.id, zw_test_utils.zwave_test_build_receive_command(CentralScene:Notification({ + scene_number = 2, + key_attributes=CentralScene.key_attributes.KEY_PRESSED_4_TIMES + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzw32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ + state_change = true + })) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should request switch level", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzw32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SwitchMultilevel:Get({}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + SensorMultilevel:Get({sensor_type = SensorMultilevel.sensor_type.ILLUMINANCE}) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }) + ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Notification:Get({notification_type = Notification.notification_type.HOME_SECURITY, event = Notification.event.home_security.MOTION_DETECTION}) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..40213851c7 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua @@ -0,0 +1,335 @@ +-- Copyright 2025 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 Configuration = (require "st.zwave.CommandClass.Configuration")({version=4}) +local t_utils = require "integration_test.utils" +local st_device = require "st.device" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + {value = zw.SWITCH_BINARY}, + {value = zw.SWITCH_MULTILEVEL}, + {value = zw.BASIC}, + {value = zw.CONFIGURATION}, + {value = zw.CENTRAL_SCENE}, + {value = zw.ASSOCIATION}, + } + } +} + +-- Create mock parent device +local mock_parent_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +-- Create mock child device (notification device) +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +-- Set child device network type +mock_child_device.network_type = st_device.NETWORK_TYPE_CHILD + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 0, -- Switch off sends 0 + size = 4 + }) + ) + ) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = math.random(0, 100) -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = notificationValue, + size = 4 + }) + ) + ) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + local temp = math.random(2700, 6500) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { temp } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(temp)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_parent_device, + Configuration:Set({ + parameter_number = 99, + configuration_value = 33514751, -- Calculated: effect(1)*16777216 + hue(255)*65536 + level(100)*256 + 255 + size = 4 + }) + ) + ) + end +) + +test.run_registered_tests() 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 new file mode 100644 index 0000000000..8cccb5726e --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua @@ -0,0 +1,151 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) +local t_utils = require "integration_test.utils" + +-- Inovelli VZW32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = 0x031E +local INOVELLI_VZW32_SN_PRODUCT_TYPE = 0x0017 +local INOVELLI_VZW32_SN_PRODUCT_ID = 0x0001 + +-- Device endpoints with supported command classes +local inovelli_vzw32_sn_endpoints = { + { + command_classes = { + { value = zw.SWITCH_BINARY }, + { value = zw.SWITCH_MULTILEVEL }, + { value = zw.BASIC }, + { value = zw.CONFIGURATION }, + { value = zw.CENTRAL_SCENE }, + { value = zw.ASSOCIATION }, + } + } +} + +-- Create mock device +local mock_inovelli_vzw32_sn = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("inovelli-mmwave-dimmer-vzw32-sn.yml"), + zwave_endpoints = inovelli_vzw32_sn_endpoints, + zwave_manufacturer_id = INOVELLI_MANUFACTURER_ID, + zwave_product_type = INOVELLI_VZW32_SN_PRODUCT_TYPE, + zwave_product_id = INOVELLI_VZW32_SN_PRODUCT_ID +}) + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzw32_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter 1 (example preference) +do + local new_param_value = 10 + test.register_coroutine_test( + "Parameter 1 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 1, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 52 (example preference) +do + local new_param_value = 25 + test.register_coroutine_test( + "Parameter 52 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 52, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 158 (example preference) +do + local new_param_value = 5 + test.register_coroutine_test( + "Parameter 158 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter158 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 158, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + end + ) +end + +-- Test parameter 101 (2-byte parameter) +do + local new_param_value = -400 + test.register_coroutine_test( + "Parameter 101 should be updated in the device configuration after change", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_inovelli_vzw32_sn, + Configuration:Set({ + parameter_number = 101, + configuration_value = new_param_value, + size = 2 + }) + ) + ) + end + ) +end + +-- Test notificationChild preference (special case for child device creation) +do + local new_param_value = true + test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzw32_sn:generate_info_changed({preferences = {notificationChild = new_param_value}})) + + -- Expect child device creation + mock_inovelli_vzw32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "nil Notification", -- This will be the parent label + "Notification" + profile = "rgbw-bulb", + parent_device_id = mock_inovelli_vzw32_sn.id, + parent_assigned_child_key = "notification" + }) + end + ) +end + +test.run_registered_tests() \ No newline at end of file From 620b21cfc711c00e5f66cb5b6f936533788939b6 Mon Sep 17 00:00:00 2001 From: cao wei Date: Thu, 27 Nov 2025 10:38:09 +0800 Subject: [PATCH 290/449] Add Wistar Smart Curtain Motor Signed-off-by: cao wei --- .../matter-window-covering/fingerprints.yml | 30 +++++++++++++++++++ tools/localizations/cn.csv | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 3f84380085..532ba1f304 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -177,6 +177,36 @@ matterManufacturer: vendorId: 0x1457 productId: 0x0018 deviceProfileName: window-covering-tilt + - id: "5207/20" + deviceLabel: WISTAR WSCMQ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0014 + deviceProfileName: window-covering + - id: "5207/21" + deviceLabel: WISTAR WSCMXI Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0015 + deviceProfileName: window-covering + - id: "5207/32" + deviceLabel: WISTAR WSCMT Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0020 + deviceProfileName: window-covering + - id: "5207/34" + deviceLabel: WISTAR WSCMXB Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0022 + deviceProfileName: window-covering + - id: "5207/35" + deviceLabel: WISTAR WSCMXC Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0023 + deviceProfileName: window-covering + - id: "5207/38" + deviceLabel: WISTAR WSCMXJ Smart Curtain Motor + vendorId: 0x1457 + productId: 0x0026 + deviceProfileName: window-covering #Yooksmart - id: "5411/1052" deviceLabel: Smart WindowCovering Series diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index afa2fbc811..e1ea39ff0c 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -122,4 +122,10 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "VIVIDSTORM Smart Screen VWSDSTUST120H",VIVIDSTORM智能幕布 VWSDSTUST120H "HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 "Yanmi Switch (3 Way)",岩米三位智能开关面板 -"Onvis Smart Plug S4EU",Onvis 智能插座S4EU \ No newline at end of file +"Onvis Smart Plug S4EU",Onvis 智能插座S4EU +"WISTAR WSCMQ Smart Curtain Motor",威仕达智能开合帘电机 WSCMQ +"WISTAR WSCMXI Smart Curtain Motor",威仕达智能开合帘电机 WSCMXI +"WISTAR WSCMT Smart Curtain Motor",威仕达智能开合帘电机 WSCMT +"WISTAR WSCMXB Smart Curtain Motor",威仕达智能开合帘电机 WSCMXB +"WISTAR WSCMXC Smart Curtain Motor",威仕达智能开合帘电机 WSCMXC +"WISTAR WSCMXJ Smart Curtain Motor",威仕达智能开合帘电机 WSCMXJ From 641a5a7cabc1cff02db63524c407d6da04723386 Mon Sep 17 00:00:00 2001 From: FrankSpringfield Date: Fri, 28 Nov 2025 16:09:03 +0800 Subject: [PATCH 291/449] [PLM251120-09263][PLM251120-09698] Hide some specific events in history Signed-off-by: FrankSpringfield --- .../zigbee-bed/src/shus-mattress/init.lua | 6 +-- .../src/test/test_shus_mattress.lua | 50 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua index c11354a8d2..08109f35ce 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua @@ -36,7 +36,7 @@ end local function process_control_attr_factory(cmd) return function(driver, device, value, zb_rx) - device:emit_event(cmd.idle()) + device:emit_event(cmd("idle", { visibility = { displayed = false }})) end end @@ -129,7 +129,7 @@ local function process_capabilities_hardness_factory(cap,attrs,cap_attr) ) --A button that can be triggered continuously local evt_ctrl = cap_attr.soft() - local evt_idle = cap_attr.idle() + local evt_idle = cap_attr("idle", { visibility = { displayed = false }}) if cmd.args[cap] == "hard" then evt_ctrl = cap_attr.hard() end @@ -148,7 +148,7 @@ local function device_init(driver, device) end local function device_added(driver, device) - device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"})) + device:emit_event(custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }})) do_refresh(driver, device) end 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 0ba0441719..d3922a3bae 100755 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}) )) + custom_capabilities.yoga.supportedYogaState({"stop", "left", "right"}, { visibility = { displayed = false }}) )) local read_0x0006_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0006, MFG_CODE) local read_0x0007_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0007, MFG_CODE) local read_0x0009_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0009, MFG_CODE) @@ -140,7 +140,7 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 0 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 0 } @@ -150,12 +150,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle()", + "Device reported leftback 1 and driver emit custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0000, data_types.Uint8.ID, 1 } @@ -165,12 +165,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftback.idle())) + custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 0 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 0 } @@ -180,12 +180,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle()", + "Device reported leftwaist 1 and driver emit custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0001, data_types.Uint8.ID, 1 } @@ -195,12 +195,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.leftwaist.idle())) + custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 0 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 0 } @@ -210,12 +210,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle()", + "Device reported lefthip 1 and driver emit custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0002, data_types.Uint8.ID, 1 } @@ -225,12 +225,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.left_control.lefthip.idle())) + custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 0 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 0 } @@ -240,12 +240,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle()", + "Device reported rightback 1 and driver emit custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0003, data_types.Uint8.ID, 1 } @@ -255,12 +255,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightback.idle())) + custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 0 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 0 } @@ -270,12 +270,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle()", + "Device reported rightwaist 1 and driver emit custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0004, data_types.Uint8.ID, 1 } @@ -285,12 +285,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.rightwaist.idle())) + custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 0 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 0 } @@ -300,12 +300,12 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) test.register_coroutine_test( - "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle()", + "Device reported righthip 1 and driver emit custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }})", function() local attr_report_data = { { 0x0005, data_types.Uint8.ID, 1 } @@ -315,7 +315,7 @@ test.register_coroutine_test( zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", - custom_capabilities.right_control.righthip.idle())) + custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end ) From 76f1f1a1fdb6df0a9db766e8e7aeec2bfab664f5 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 11:34:12 -0800 Subject: [PATCH 292/449] WWSTCERT-9170 Linkind Smart Light Bulb --- 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 0bc24ec7ca..78817141db 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -131,6 +131,11 @@ matterManufacturer: vendorId: 0x1396 productId: 0x1001 deviceProfileName: light-color-level-fan + - id: "5014/4214" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1076 + deviceProfileName: light-color-level #Bosch Smart Home - id: "4617/12310" deviceLabel: Plug Compact [M] From 6c3399293950ae09363b0a4a23e392f579425b5b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 11:31:49 -0800 Subject: [PATCH 293/449] WWSTCERT-9153 Leviton Decora Smart Wi-Fi (3rd Gen) 15A Switch WWSTCERT-9156 Leviton Decora Smart Wi-Fi (3rd Gen) 600W Dimmer --- 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 0bc24ec7ca..25c3807042 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -646,6 +646,16 @@ matterManufacturer: vendorId: 0x109B productId: 0x1007 deviceProfileName: 3-button + - id: "4251/4113" + deviceLabel: "Decora Smart Wi-Fi (3rd Gen) 15A Switch" + vendorId: 0x109B + productId: 0x1011 + deviceProfileName: switch-binary + - id: "4251/4112" + deviceLabel: "Decora Smart Wi-Fi (3rd Gen) 600W Dimmer" + vendorId: 0x109B + productId: 0x1010 + deviceProfileName: switch-level #LeTianPai - id: "5163/4097" deviceLabel: LeTianPai Smart Light Bulb From ee41ab878c755261590c27c58b446346cf32291b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 11:37:26 -0800 Subject: [PATCH 294/449] WWSTCERT-9159 Osram SMART MAT B40 TW 827 FR E14 --- 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 0bc24ec7ca..29cb37f633 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -940,6 +940,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x092F deviceProfileName: light-color-level + - id: "4489/2715" + deviceLabel: SMART MAT B40 TW 827 FR E14 + vendorId: 0x1189 + productId: 0x0A9B + deviceProfileName: light-level-colorTemperature #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 9ec87af8e0b913a3878a070e49e1bfd55cab00f3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 11:44:48 -0800 Subject: [PATCH 295/449] WWSTCERT-9195 WiZ Gradient Light Bars WWSTCERT-9198 WiZ Gradient Light Bars WWSTCERT-9209 WiZ Gradient Floor lamp WWSTCERT-9212 WiZ Gradient Floor lamp --- .../matter-switch/fingerprints.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 0bc24ec7ca..b63534b146 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2855,6 +2855,26 @@ matterManufacturer: vendorId: 0x100b productId: 0x228F deviceProfileName: light-color-level-2200K-6500K + - id: "4107/8954" + deviceLabel: WiZ Gradient Light Bars + vendorId: 0x100B + productId: 0x22FA + deviceProfileName: light-color-level + - id: "4107/8953" + deviceLabel: WiZ Gradient Light Bars + vendorId: 0x100B + productId: 0x22F9 + deviceProfileName: light-color-level + - id: "4107/8949" + deviceLabel: WiZ Gradient Floor lamp + vendorId: 0x100B + productId: 0x22F5 + deviceProfileName: light-color-level + - id: "4107/8948" + deviceLabel: WiZ Gradient Floor lamp + vendorId: 0x100B + productId: 0x22F4 + deviceProfileName: light-color-level #Zemismart - id: "5020/61154" deviceLabel: Zemismart Inline Module From ca527452360c9a61d88f3a0fbc2ae6c0c96106fb Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 11:48:43 -0800 Subject: [PATCH 296/449] WWSTCERT-9060 Resideo Valve Controller --- drivers/SmartThings/zigbee-thermostat/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index 1dc993ea26..d88b3ff89a 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -124,6 +124,11 @@ zigbeeManufacturer: manufacturer: Resideo Korea model: DT300ST-M000 deviceProfileName: thermostat-resideo-dt300st-m000 + - id: "Resideo Korea/MC200ST" + deviceLabel: Valve Controller + manufacturer: Resideo Korea + model: MC200ST + deviceProfileName: thermostat-resideo-dt300st-m000 - id: "LUMI/lumi.airrtc.agl001" deviceLabel: Aqara Smart Radiator Thermostat E1 manufacturer: LUMI From 16fbe0013cef4b6c69a3ea8a15a5293e2a6fdae7 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 24 Nov 2025 11:29:30 -0800 Subject: [PATCH 297/449] Merge pull request #2567 from SmartThingsCommunity/new_device/WWSTCERT-9021 WWSTCERT-9021 Aqara Presence Multi-Sensor FP300 --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index c1e2f2dc04..36ec8b6bc8 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -10,6 +10,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2003 deviceProfileName: motion-illuminance-battery + - id: "4447/8197" + deviceLabel: Presence Multi-Sensor FP300 + vendorId: 0x115F + productId: 0x2005 + deviceProfileName: motion-illuminance-temperature-humidity-batteryLevel #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" From ff7d391865fb288e01e629254e6ac447d9574eeb Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 26 Nov 2025 10:34:13 -0800 Subject: [PATCH 298/449] Merge pull request #2576 from SmartThingsCommunity/new_device/WWSTCERT-9074 WWSTCERT-9074 Cync Fan Switch --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 8358e3164e..92a8ea3207 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -16,6 +16,12 @@ matterManufacturer: vendorId: 0x1209 productId: 0x3012 deviceProfileName: thermostat-heating-only-nostate + #Cync + - id: "4921/121" + deviceLabel: Cync Fan Switch + vendorId: 0x1339 + productId: 0x0079 + deviceProfileName: fan-generic #Eve - id: "4874/79" deviceLabel: Eve Thermo From e00f6714bff79da75706c9d1232baf05836349c2 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 26 Nov 2025 10:44:18 -0800 Subject: [PATCH 299/449] WWSTCERT-9105 Meross Smart Presence Sensor (Thread) (#2578) * WWSTCERT-9105 Meross Smart Presence Sensor (Thread) * use non-deprecated profile * use correct profile name --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 36ec8b6bc8..3e1546dd54 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -128,6 +128,11 @@ matterManufacturer: vendorId: 0x1345 productId: 0x4201 deviceProfileName: motion-illuminance + - id: "4933/16898" + deviceLabel: Smart Presence Sensor (Thread) + vendorId: 0x1345 + productId: 0x4202 + deviceProfileName: motion-illuminance-battery # Neo - id: "4991/1122" deviceLabel: Door Sensor From e3ce206b159138a50d301344b61bc26d1303f966 Mon Sep 17 00:00:00 2001 From: Tom Manley Date: Tue, 25 Nov 2025 13:34:51 -0600 Subject: [PATCH 300/449] Merge pull request #2580 from SmartThingsCommunity/feature/fp300 WWSTCERT-9021 Aqara Presence Multi-Sensor FP300 (update profile) --- drivers/SmartThings/matter-sensor/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 3e1546dd54..8836c276cd 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -14,7 +14,7 @@ matterManufacturer: deviceLabel: Presence Multi-Sensor FP300 vendorId: 0x115F productId: 0x2005 - deviceProfileName: motion-illuminance-temperature-humidity-batteryLevel + deviceProfileName: presence-illuminance-temperature-humidity-battery #Bosch - id: 4617/12309 deviceLabel: "Door/window contact II [M]" From 9380f9c2149fb6ac351a7ff506ad2c3a65ae49e5 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 14:10:00 -0800 Subject: [PATCH 301/449] Revert "Merge branch 'beta' into main" This reverts commit 48c52abf7835f140f03a8bdf0da56095b0186503, reversing changes made to d978bac0aadac7c9f88a54c0e1ba4580a29b312b. --- .../camera/camera_utils/device_configuration.lua | 7 +++---- .../matter-switch/src/test/test_matter_camera.lua | 2 +- 2 files changed, 4 insertions(+), 5 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 a54f50b5b0..90c5ee3498 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 @@ -55,10 +55,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local camera_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.CAMERA) if #camera_endpoints > 0 then - if #device:get_endpoints(clusters.WebRTCTransportProvider.ID, {cluster_type = "SERVER"}) > 0 and - #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then - table.insert(main_component_capabilities, capabilities.webrtc.ID) - end local camera_ep = switch_utils.get_endpoint_info(device, camera_endpoints[1]) for _, ep_cluster in pairs(camera_ep.clusters or {}) do if ep_cluster.cluster_id == clusters.CameraAvStreamManagement.ID and has_server_cluster_type(ep_cluster) then @@ -110,6 +106,9 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr 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) and + #device:get_endpoints(clusters.WebRTCTransportRequestor.ID, {cluster_type = "CLIENT"}) > 0 then + table.insert(main_component_capabilities, capabilities.webrtc.ID) end end 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 d504107c79..b9804a2a76 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -156,7 +156,6 @@ local function update_device_profile() { "main", { - "webrtc", "videoCapture2", "cameraViewportSettings", "localMediaStorage", @@ -168,6 +167,7 @@ local function update_device_profile() "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", + "webrtc", "motionSensor", "sounds", } From afbb8b3afeaad764d0aa5aaee4688272adbc8157 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 1 Dec 2025 14:40:00 -0800 Subject: [PATCH 302/449] fix timing issue with aqara test --- drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua | 1 + 1 file changed, 1 insertion(+) 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 6996f05096..838b9545fb 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -270,6 +270,7 @@ test.register_coroutine_test( local attr_report_data = { { PRIVATE_ATTRIBUTE_ID_ALIVE, data_types.OctetString.ID, "\x01\x21\xB8\x0B\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x45\x08\x06\x24\x07\x00\x00\x00\x00\x08\x21\x15\x01\x0A\x21\xF5\x65\x0C\x20\x01\x64\x20\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00" } } + test.wait_for_events() test.socket.zigbee:__queue_receive({ mock_device_h1_double_rocker.id, zigbee_test_utils.build_attribute_report(mock_device_h1_double_rocker, PRIVATE_CLUSTER_ID, attr_report_data, From 1f59baaf2316661d8b9313555ee3ad70bb9a2b2a Mon Sep 17 00:00:00 2001 From: haedo-doo Date: Wed, 3 Dec 2025 08:40:56 +0900 Subject: [PATCH 303/449] WWSTCERT-8461 Aqara dimmer controller t1 (#2468) * add Aqara Dimmer Controller T1(0-10v) * Change the color temperature range without adding a profile * Fix test_aqara_led_bulb bug * fix deviceLabel format * add the new model instead of duplicating * fix preference bug * Copy the issue event to the added handler * Simplify the get_device_parameters code * add emit_event_if_latest_state_missing check code * remove unused function is_aqara_products in int.lua * Move the new model to can_handle.lua. --- .../zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/profiles/aqara-light.yml | 4 ---- .../src/aqara-light/can_handle.lua | 3 ++- .../zigbee-switch/src/aqara-light/init.lua | 21 +++++++++++++++++++ .../zigbee-switch/src/preferences.lua | 18 +++++++++++----- .../src/test/test_aqara_led_bulb.lua | 3 +++ .../src/test/test_aqara_light.lua | 3 +++ 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 3dfa9970b5..4c91da651e 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -116,6 +116,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.light.acn004 deviceProfileName: aqara-light + - id: "LUMI/lumi.light.cwacn1" + deviceLabel: Aqara Smart Dimmer Controller T1 (0-10v) + manufacturer: LUMI + model: lumi.light.cwacn1 + deviceProfileName: aqara-light - id: "Aqara/lumi.light.acn014" deviceLabel: Aqara LED Light Bulb T1 (Tunable White) manufacturer: Aqara diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml index 3b4462bce2..6c2da08393 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-light.yml @@ -12,10 +12,6 @@ components: range: [1, 100] - id: colorTemperature version: 1 - config: - values: - - key: "colorTemperature.value" - range: [2700, 6000] - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua index 73ea4c3d00..7b20caf5c7 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/can_handle.lua @@ -4,7 +4,8 @@ return function(opts, driver, device) local FINGERPRINTS = { { mfr = "LUMI", model = "lumi.light.acn004" }, - { mfr = "Aqara", model = "lumi.light.acn014" } + { mfr = "Aqara", model = "lumi.light.acn014" }, + { mfr = "LUMI", model = "lumi.light.cwacn1" } } for _, fingerprint in ipairs(FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 760ffebebf..06d4bf1cc7 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -21,9 +21,21 @@ local function do_refresh(self, device) device:send(ColorControl.attributes.ColorTemperatureMireds:read(device)) end +local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) + if device:get_latest_state(component, capability.ID, attribute_name) == nil then + device:emit_event(value) + end +end + local function device_added(driver, device, event) device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1)) -- private + + local value = { minimum = 2700, maximum = 6000 } + if device:get_model() == "lumi.light.cwacn1" then + value.maximum = 6500 + end + emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) end local function do_configure(self, device) @@ -42,9 +54,18 @@ local function set_level_handler(driver, device, cmd) device:send(Level.commands.MoveToLevelWithOnOff(device, level, dimming_rate)) end +local function init(self, device) + local value = { minimum = 2700, maximum = 6000 } + if device:get_model() == "lumi.light.cwacn1" then + value.maximum = 6500 + end + emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) +end + local aqara_light_handler = { NAME = "Aqara Light Handler", lifecycle_handlers = { + init = init, added = device_added, doConfigure = do_configure }, diff --git a/drivers/SmartThings/zigbee-switch/src/preferences.lua b/drivers/SmartThings/zigbee-switch/src/preferences.lua index 9b8a0cb66d..0e9c8f5b87 100644 --- a/drivers/SmartThings/zigbee-switch/src/preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/preferences.lua @@ -7,7 +7,10 @@ local data_types = require "st.zigbee.data_types" local devices = { AQARA_LIGHT = { - MATCHING_MATRIX = { mfr = "LUMI", model = "lumi.light.acn004" }, + MATCHING_MATRIX = { + { mfr = "LUMI", model = "lumi.light.acn004" }, + { mfr = "LUMI", model = "lumi.light.cwacn1" } + }, PARAMETERS = { ["stse.restorePowerState"] = function(device, value) return cluster_base.write_manufacturer_specific_attribute(device, 0xFCC0, @@ -28,7 +31,7 @@ local devices = { } }, AQARA_LIGHT_BULB = { - MATCHING_MATRIX = { mfr = "Aqara", model = "lumi.light.acn014" }, + MATCHING_MATRIX = {{ mfr = "Aqara", model = "lumi.light.acn014" }}, PARAMETERS = { ["stse.restorePowerState"] = function(device, value) return cluster_base.write_manufacturer_specific_attribute(device, 0xFCC0, @@ -64,12 +67,17 @@ preferences.sync_preferences = function(driver, device) end preferences.get_device_parameters = function(zigbee_device) + local mfr = zigbee_device:get_manufacturer() + local model = zigbee_device:get_model() + for _, device in pairs(devices) do - if zigbee_device:get_manufacturer() == device.MATCHING_MATRIX.mfr and - zigbee_device:get_model() == device.MATCHING_MATRIX.model then - return device.PARAMETERS + for _, fp in ipairs(device.MATCHING_MATRIX) do + if fp.mfr == mfr and fp.model == model then + return device.PARAMETERS + end end end + return nil end 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 8584e74e1d..b2f9359850 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 @@ -7,6 +7,7 @@ local clusters = require "st.zigbee.zcl.clusters" 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 capabilities = require "st.capabilities" local OnOff = clusters.OnOff local Level = clusters.Level @@ -37,6 +38,7 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -50,6 +52,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) 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 814de3f055..940131b3e1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -7,6 +7,7 @@ local clusters = require "st.zigbee.zcl.clusters" 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 capabilities = require "st.capabilities" local OnOff = clusters.OnOff local Level = clusters.Level @@ -39,6 +40,7 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -52,6 +54,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) From d723c873887d6acba882e3d1a1cf3ccd3f98ec91 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Thu, 13 Nov 2025 01:34:44 -0600 Subject: [PATCH 304/449] CHAD-17042:Lazy loading v2 of sub drivers and copyright updates --- .../src/aqara/aqara_utils.lua | 3 ++ .../src/aqara/can_handle.lua | 14 +++++ .../src/aqara/fingerprints.lua | 10 ++++ .../high-precision-motion/can_handle.lua | 9 ++++ .../src/aqara/high-precision-motion/init.lua | 7 +-- .../zigbee-motion-sensor/src/aqara/init.lua | 22 ++------ .../src/aqara/sub_drivers.lua | 8 +++ .../src/aurora/can_handle.lua | 11 ++++ .../zigbee-motion-sensor/src/aurora/init.lua | 20 ++------ .../src/battery-voltage/can_handle.lua | 14 +++++ .../src/battery-voltage/fingerprints.lua | 12 +++++ .../src/battery-voltage/init.lua | 35 ++----------- .../src/centralite/can_handle.lua | 11 ++++ .../src/centralite/init.lua | 20 ++------ .../src/compacta/can_handle.lua | 11 ++++ .../src/compacta/init.lua | 20 ++------ .../src/frient/can_handle.lua | 14 +++++ .../src/frient/fingerprints.lua | 10 ++++ .../zigbee-motion-sensor/src/frient/init.lua | 32 ++---------- .../src/gatorsystem/can_handle.lua | 11 ++++ .../src/gatorsystem/init.lua | 20 ++------ .../src/ikea/can_handle.lua | 14 +++++ .../src/ikea/fingerprints.lua | 8 +++ .../zigbee-motion-sensor/src/ikea/init.lua | 29 ++--------- .../zigbee-motion-sensor/src/init.lua | 51 ++++++++----------- .../src/iris/can_handle.lua | 14 +++++ .../src/iris/fingerprints.lua | 8 +++ .../zigbee-motion-sensor/src/iris/init.lua | 29 ++--------- .../src/lazy_load_subdriver.lua | 15 ++++++ .../src/motion_timeout/can_handle.lua | 14 +++++ .../src/motion_timeout/fingerprints.lua | 10 ++++ .../src/motion_timeout/init.lua | 34 +++---------- .../src/nyce/can_handle.lua | 14 +++++ .../src/nyce/fingerprints.lua | 10 ++++ .../zigbee-motion-sensor/src/nyce/init.lua | 31 ++--------- .../src/samjin/can_handle.lua | 11 ++++ .../zigbee-motion-sensor/src/samjin/init.lua | 20 ++------ .../src/sengled/can_handle.lua | 14 +++++ .../src/sengled/fingerprints.lua | 8 +++ .../zigbee-motion-sensor/src/sengled/init.lua | 16 ++---- .../src/smartsense/can_handle.lua | 18 +++++++ .../src/smartsense/init.lua | 29 ++--------- .../src/smartthings/can_handle.lua | 11 ++++ .../src/smartthings/init.lua | 20 ++------ .../src/st/zigbee/zdo/init.lua | 20 ++------ .../test_all_capabilities_zigbee_motion.lua | 16 ++---- .../src/test/test_aqara_high_precision.lua | 16 ++---- .../test/test_aqara_motion_illuminance.lua | 16 ++---- .../src/test/test_aurora_motion.lua | 16 ++---- .../src/test/test_battery_voltage_motion.lua | 16 ++---- .../src/test/test_centralite_motion.lua | 16 ++---- .../src/test/test_compacta_motion.lua | 16 ++---- .../src/test/test_frient_motion_sensor.lua | 16 ++---- .../test/test_frient_motion_sensor2_pet.lua | 16 ++---- .../test/test_frient_motion_sensor_pro.lua | 15 +----- .../src/test/test_gator_motion.lua | 16 ++---- .../src/test/test_ikea_motion.lua | 20 ++------ .../src/test/test_samjin_sensor.lua | 16 ++---- .../src/test/test_sengled_motion.lua | 16 ++---- .../test/test_smartsense_motion_sensor.lua | 30 ++++------- .../src/test/test_smartthings_motion.lua | 16 ++---- .../src/test/test_thirdreality_sensor.lua | 16 ++---- .../src/test/test_zigbee_motion_iris.lua | 16 ++---- .../src/test/test_zigbee_motion_nyce.lua | 16 ++---- .../src/test/test_zigbee_motion_orvibo.lua | 16 ++---- .../test/test_zigbee_plugin_motion_sensor.lua | 16 ++---- .../src/thirdreality/can_handle.lua | 14 +++++ .../src/thirdreality/fingerprints.lua | 9 ++++ .../src/thirdreality/init.lua | 30 ++--------- .../can_handle.lua | 14 +++++ .../fingerprints.lua | 8 +++ .../src/zigbee-plugin-motion-sensor/init.lua | 29 ++--------- 72 files changed, 524 insertions(+), 695 deletions(-) create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua index e48f8fde92..8a1b184693 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/aqara_utils.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local aqara_utils = {} diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..da7a97c64b --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..705fa72746 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.motion.ac02" }, + { mfr = "LUMI", model = "lumi.motion.agl02" }, + { mfr = "LUMI", model = "lumi.motion.agl04" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua new file mode 100644 index 0000000000..02c2f313cb --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "lumi.motion.agl04" then + return true, require("aqara.high-precision-motion") + end + return false +end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua index 8c70857bdb..d6c89e3e0c 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/high-precision-motion/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -118,9 +121,7 @@ local aqara_high_precision_motion_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.motion.agl04" - end + can_handle = require("aqara.high-precision-motion.can_handle") } return aqara_high_precision_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua index 1746b21953..e4586d4b0f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_commands = require "st.zigbee.zcl.global_commands" local clusters = require "st.zigbee.zcl.clusters" @@ -16,11 +19,6 @@ local FREQUENCY_ATTRIBUTE_ID = 0x0102 local MOTION_DETECTED_UINT32 = 65536 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.motion.ac02" }, - { mfr = "LUMI", model = "lumi.motion.agl02" }, - { mfr = "LUMI", model = "lumi.motion.agl04" } -} local CONFIGURATIONS = { { @@ -33,14 +31,6 @@ local CONFIGURATIONS = { } } -local is_aqara_products = function(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 motion_illuminance_attr_handler(driver, device, value, zb_rx) -- The low 16 bits for Illuminance @@ -123,10 +113,8 @@ local aqara_motion_handler = { } } }, - sub_drivers = { - require("aqara.high-precision-motion") - }, - can_handle = is_aqara_products + sub_drivers = require("aqara.sub_drivers"), + can_handle = require("aqara.can_handle"), } return aqara_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..a0e42d2085 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aqara/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load = require "lazy_load_subdriver" + +return { + lazy_load("aqara.high-precision-motion") +} diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua new file mode 100644 index 0000000000..f58429a53c --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function aurora_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Aurora" and device:get_model() == "MotionSensor51AU" then + return true, require("aurora") + end + return false +end + +return aurora_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua index 060ed9d308..d08eb13c36 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/aurora/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local function added_handler(self, device) @@ -24,9 +14,7 @@ local aurora_motion_driver = { lifecycle_handlers = { added = added_handler, }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Aurora" and device:get_model() == "MotionSensor51AU" - end + can_handle = require("aurora.can_handle"), } return aurora_motion_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua new file mode 100644 index 0000000000..01bf292b1a --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local can_handle_battery_voltage = function(opts, driver, device, ...) + local FINGERPRINTS = require("battery-voltage.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("battery-voltage") + end + end + return false +end + +return can_handle_battery_voltage diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua new file mode 100644 index 0000000000..4d3006c943 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DEVICES_REPORTING_BATTERY_VOLTAGE = { + { mfr = "Bosch", model = "RFPR-ZB" }, + { mfr = "Bosch", model = "RFDL-ZB-MS" }, + { mfr = "Ecolink", model = "PIRZB1-ECO" }, + { mfr = "ADUROLIGHT", model = "VMS_ADUROLIGHT" }, + { mfr = "AduroSmart Eria", model = "VMS_ADUROLIGHT" } +} + +return DEVICES_REPORTING_BATTERY_VOLTAGE diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua index 316b626afe..d031aee153 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/battery-voltage/init.lua @@ -1,35 +1,8 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local battery_defaults = require "st.zigbee.defaults.battery_defaults" - -local DEVICES_REPORTING_BATTERY_VOLTAGE = { - { mfr = "Bosch", model = "RFPR-ZB" }, - { mfr = "Bosch", model = "RFDL-ZB-MS" }, - { mfr = "Ecolink", model = "PIRZB1-ECO" }, - { mfr = "ADUROLIGHT", model = "VMS_ADUROLIGHT" }, - { mfr = "AduroSmart Eria", model = "VMS_ADUROLIGHT" } -} -local can_handle_battery_voltage = function(opts, driver, device, ...) - for _, fingerprint in ipairs(DEVICES_REPORTING_BATTERY_VOLTAGE) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local battery_defaults = require "st.zigbee.defaults.battery_defaults" local battery_voltage_motion = { @@ -37,7 +10,7 @@ local battery_voltage_motion = { lifecycle_handlers = { init = battery_defaults.build_linear_voltage_init(2.1, 3.0) }, - can_handle = can_handle_battery_voltage + can_handle = require("battery-voltage.can_handle"), } return battery_voltage_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua new file mode 100644 index 0000000000..750bcab752 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function centralite_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "CentraLite" then + return true, require("centralite") + end + return false +end + +return centralite_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua index 6a40360224..e2492e7c78 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/centralite/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -46,9 +36,7 @@ local centralite_handler = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "CentraLite" - end + can_handle = require("centralite.can_handle"), } return centralite_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua new file mode 100644 index 0000000000..7946b15125 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function compacta_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Compacta" then + return true, require("compacta") + end + return false +end + +return compacta_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua index b303f6f8fd..85295b6391 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/compacta/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -43,9 +33,7 @@ local compacta_driver = { added = added_handler, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Compacta" - end + can_handle = require("compacta.can_handle"), } return compacta_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua new file mode 100644 index 0000000000..236ec1f3e5 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient_motion_sensor(opts, driver, device) + local FINGERPRINTS = require("frient.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient") + end + end + return false +end + +return can_handle_frient_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua new file mode 100644 index 0000000000..ddf11bbdfe --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "MOSZB-140"}, + { mfr = "frient A/S", model = "MOSZB-141"}, + { mfr = "frient A/S", model = "MOSZB-153"} +} + +return FRIENT_DEVICE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua index c067b2fe56..901dbf2cf2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/frient/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local battery_defaults = require "st.zigbee.defaults.battery_defaults" local capabilities = require "st.capabilities" @@ -35,20 +24,7 @@ local POWER_CONFIGURATION_ENDPOINT = 0x23 local TEMPERATURE_ENDPOINT = 0x26 local ILLUMINANCE_ENDPOINT = 0x27 -local FRIENT_DEVICE_FINGERPRINTS = { - { mfr = "frient A/S", model = "MOSZB-140"}, - { mfr = "frient A/S", model = "MOSZB-141"}, - { mfr = "frient A/S", model = "MOSZB-153"} -} -local function can_handle_frient_motion_sensor(opts, driver, device) - for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -214,6 +190,6 @@ local frient_motion_driver = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_frient_motion_sensor + can_handle = require("frient.can_handle"), } -return frient_motion_driver \ No newline at end of file +return frient_motion_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua new file mode 100644 index 0000000000..4257bd7ff2 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function gatorsystem_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "GatorSystem" and device:get_model() == "GSHW01" then + return true, require("gatorsystem") + end + return false +end + +return gatorsystem_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua index 1e90a65e94..ee88254233 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/gatorsystem/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -84,9 +74,7 @@ local gator_handler = { added = device_added, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "GatorSystem" and device:get_model() == "GSHW01" - end + can_handle = require("gatorsystem.can_handle"), } return gator_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua new file mode 100644 index 0000000000..7060fc8630 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ikea_motion = function(opts, driver, device) + local FINGERPRINTS = require("ikea.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ikea") + end + end + return false +end + +return is_ikea_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua new file mode 100644 index 0000000000..27162e73cf --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI motion sensor" } +} + +return IKEA_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua index b2af5e1c88..b7cc733959 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/ikea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local constants = require "st.zigbee.constants" local clusters = require "st.zigbee.zcl.clusters" @@ -26,9 +16,6 @@ local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration local Groups = clusters.Groups -local IKEA_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI motion sensor" } -} local MOTION_RESET_TIMER = "motionResetTimer" local ENTRIES_READ = "ENTRIES_READ" @@ -48,14 +35,6 @@ local function on_with_timed_off_command_handler(driver, device, zb_rx) device:set_field(MOTION_RESET_TIMER, motion_reset_timer) end -local is_ikea_motion = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function zdo_binding_table_handler(driver, device, zb_rx) for _, binding_table in pairs(zb_rx.body.zdo_body.binding_table_entries) do @@ -141,7 +120,7 @@ local ikea_motion_sensor = { added = device_added, doConfigure = do_configure }, - can_handle = is_ikea_motion + can_handle = require("ikea.can_handle"), } return ikea_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua index 69b069bf7b..11e9d94a85 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/init.lua @@ -1,22 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local constants = require "st.zigbee.constants" local zcl_clusters = require "st.zigbee.zcl.clusters" +local lazy_load_if_possible = require "lazy_load_subdriver" local temperature_measurement_defaults = { MIN_TEMP = "MIN_TEMP", @@ -110,23 +101,23 @@ local zigbee_motion_driver = { added = added_handler }, sub_drivers = { - require("aqara"), - require("aurora"), - require("ikea"), - require("iris"), - require("gatorsystem"), - require("motion_timeout"), - require("nyce"), - require("zigbee-plugin-motion-sensor"), - require("compacta"), - require("frient"), - require("samjin"), - require("battery-voltage"), - require("centralite"), - require("smartthings"), - require("smartsense"), - require("thirdreality"), - require("sengled") + lazy_load_if_possible("aqara"), + lazy_load_if_possible("aurora"), + lazy_load_if_possible("ikea"), + lazy_load_if_possible("iris"), + lazy_load_if_possible("gatorsystem"), + lazy_load_if_possible("motion_timeout"), + lazy_load_if_possible("nyce"), + lazy_load_if_possible("zigbee-plugin-motion-sensor"), + lazy_load_if_possible("compacta"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("samjin"), + lazy_load_if_possible("battery-voltage"), + lazy_load_if_possible("centralite"), + lazy_load_if_possible("smartthings"), + lazy_load_if_possible("smartsense"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("sengled"), }, additional_zcl_profiles = { [0xFC01] = true diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua new file mode 100644 index 0000000000..6d86812e50 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_iris_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("iris.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("iris") + end + end + return false +end + +return is_zigbee_iris_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua new file mode 100644 index 0000000000..2d4ebfc516 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "iMagic by GreatStar", model = "1117-S" } +} + +return ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua index 5a553b4325..f29c2772d3 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/iris/init.lua @@ -1,23 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "iMagic by GreatStar", model = "1117-S" } -} -- TODO: the IAS Zone changes should be replaced after supporting functions are included in the lua libs local do_init = function(driver, device) @@ -26,21 +13,13 @@ local do_init = function(driver, device) device:remove_configured_attribute(zcl_clusters.IASZone.ID, zcl_clusters.IASZone.attributes.ZoneStatus.ID) end -local is_zigbee_iris_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_IRIS_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local iris_motion_handler = { NAME = "Iris Motion Handler", lifecycle_handlers = { init = do_init }, - can_handle = is_zigbee_iris_motion_sensor + can_handle = require("iris.can_handle"), } return iris_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua new file mode 100644 index 0000000000..679675b85e --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("motion_timeout.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("motion_timeout") + end + end + return false +end + +return is_zigbee_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua new file mode 100644 index 0000000000..a3ff0ec226 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "ORVIBO", model = "895a2d80097f4ae2b2d40500d5e03dcc", timeout = 20 }, + { mfr = "Megaman", model = "PS601/z1", timeout = 20 }, + { mfr = "HEIMAN", model = "PIRSensor-N", timeout = 20 } +} + +return ZIGBEE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua index 0fd0e7c070..fb6f34ece4 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/motion_timeout/init.lua @@ -1,40 +1,18 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local IASZone = zcl_clusters.IASZone -local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "ORVIBO", model = "895a2d80097f4ae2b2d40500d5e03dcc", timeout = 20 }, - { mfr = "Megaman", model = "PS601/z1", timeout = 20 }, - { mfr = "HEIMAN", model = "PIRSensor-N", timeout = 20 } -} -local is_zigbee_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local generate_event_from_zone_status = function(driver, device, zone_status, zigbee_message) + local FINGERPRINTS = require("motion_timeout.fingerprints") device:emit_event( (zone_status:is_alarm1_set() or zone_status:is_alarm2_set()) and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then device.thread:call_with_delay(fingerprint.timeout, function(d) device:emit_event(capabilities.motionSensor.motion.inactive()) @@ -66,7 +44,7 @@ local motion_timeout_handler = { } } }, - can_handle = is_zigbee_motion_sensor + can_handle = require("motion_timeout.can_handle"), } return motion_timeout_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua new file mode 100644 index 0000000000..5c603cfef2 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_nyce_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("nyce.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("nyce") + end + end + return false +end + +return is_zigbee_nyce_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua new file mode 100644 index 0000000000..ecb5193f99 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "NYCE", model = "3041" }, + { mfr = "NYCE", model = "3043" }, + { mfr = "NYCE", model = "3045" } +} + +return ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua index 8a8196afa6..71efbf3e23 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/nyce/init.lua @@ -1,36 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local OccupancySensing = zcl_clusters.OccupancySensing -local ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "NYCE", model = "3041" }, - { mfr = "NYCE", model = "3043" }, - { mfr = "NYCE", model = "3045" } -} -local is_zigbee_nyce_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_NYCE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event( @@ -49,7 +26,7 @@ local nyce_motion_handler = { } } }, - can_handle = is_zigbee_nyce_motion_sensor + can_handle = require("nyce.can_handle"), } return nyce_motion_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua new file mode 100644 index 0000000000..1ad7fe5f7a --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" then + return true, require("samjin") + end + return false +end + +return samjin_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua index 66fe8b4491..5d3ff95fd0 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/samjin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -44,9 +34,7 @@ local samjin_driver = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" - end + can_handle = require("samjin.can_handle"), } return samjin_driver diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..bae9a89938 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E1M-G7H" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua index 0e2575415f..5c2402f32e 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E1M-G7H" } -} local CONFIGURATIONS = { { @@ -27,14 +27,6 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(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_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) @@ -49,7 +41,7 @@ local sengled_motion_sensor_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_motion_sensor_handler diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua new file mode 100644 index 0000000000..e85561680b --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + + local SMARTSENSE_MFR = "SmartThings" + local SMARTSENSE_MODEL = "PGC314" + local SMARTSENSE_PROFILE_ID = 0xFC01 + + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] + if (device:get_manufacturer() == SMARTSENSE_MFR and device:get_model() == SMARTSENSE_MODEL) or + endpoint.profile_id == SMARTSENSE_PROFILE_ID then + return true, require("smartsense") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua index 78f570872f..df48f6af95 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartsense/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -18,9 +8,6 @@ local utils = require "st.utils" local motion = capabilities.motionSensor.motion local signalStrength = capabilities.signalStrength -local SMARTSENSE_MFR = "SmartThings" -local SMARTSENSE_MODEL = "PGC314" -local SMARTSENSE_PROFILE_ID = 0xFC01 local SMARTSENSE_MOTION_CLUSTER = 0xFC04 local SMARTSENSE_MOTION_STATUS_CMD = 0x00 local MOTION_MASK = 0x02 @@ -43,14 +30,6 @@ local battery_table = { [0] = 0 } -local function can_handle(opts, driver, device, ...) - local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] - if (device:get_manufacturer() == SMARTSENSE_MFR and device:get_model() == SMARTSENSE_MODEL) or - endpoint.profile_id == SMARTSENSE_PROFILE_ID then - return true - end - return false -end local function device_added(driver, device) device:emit_event(motion.inactive()) @@ -96,7 +75,7 @@ local smartsense_motion = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle + can_handle = require("smartsense.can_handle"), } return smartsense_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua new file mode 100644 index 0000000000..747f859062 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function smartthings_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SmartThings" then + return true, require("smartthings") + end + return false + end + +return smartthings_can_handle diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua index 586378edc5..0907e7de16 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/smartthings/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -44,9 +34,7 @@ local smartthings_motion = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SmartThings" - end + can_handle = require("smartthings.can_handle"), } return smartthings_motion diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua index 9d009d47cb..eb551c84fa 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/st/zigbee/zdo/init.lua @@ -1,16 +1,6 @@ --- Copyright 2021 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. +-- Copyright 2021 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local utils = require "st.zigbee.utils" local zdo_commands = require "st.zigbee.zdo.commands" @@ -101,7 +91,7 @@ function ZdoMessageBody.deserialize(parent, buf) -- binding table entry endpoint_id local version = require "version" if version.rpc == 8 then - buf.buf = buf.buf .. "\01" + buf.buf = buf.buf .. "" buf:seek(1) end s.zdo_body = zdo_commands.parse_zdo_command(parent.address_header.cluster.value, buf) @@ -152,4 +142,4 @@ end setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) -return zdo_messages \ No newline at end of file +return zdo_messages 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 b016503f44..948d433c9f 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 4ec5b9a3f3..e7bfcbf642 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 52b437f86e..2580af0ceb 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 200d8e544b..1a5a292fc3 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 5878d9e687..737a594406 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 2fafad786b..1e35976fac 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 2e50c1a615..0660661d80 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 9b973b1724..89d014dc90 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "base64" 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 0f142ea6cb..6869412d49 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "base64" 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 cda87474fa..9556a6e58e 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "base64" 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 2ae1fe0952..7f47416e93 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 133fe63dbb..0173ac431d 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -133,7 +123,7 @@ test.register_coroutine_test( test.register_coroutine_test( "ZDO Message handler and adding hub to group", function() - local binding_table = mgmt_bind_response.BindingTableListRecord("\x6A\x9D\xC0\xFE\xFF\x5E\xCF\xD0", 0x01, 0x0006, 0x01, 0xB9F2) + local binding_table = mgmt_bind_response.BindingTableListRecord("j^", 0x01, 0x0006, 0x01, 0xB9F2) local response = mgmt_bind_response.MgmtBindResponse({ status = 0x00, total_binding_table_entry_count = 0x01, @@ -154,7 +144,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Request all binding table entries and fall back to group 0x0000", function() - local binding_table_long = mgmt_bind_response.BindingTableListRecord("\x6A\x9D\xC0\xFE\xFF\x5E\xCF\xD0", 0x01, 0x0006, 0x03, "DEADBEEF", 0x01) + local binding_table_long = mgmt_bind_response.BindingTableListRecord("j^", 0x01, 0x0006, 0x03, "DEADBEEF", 0x01) local response = mgmt_bind_response.MgmtBindResponse({ status = 0x00, total_binding_table_entry_count = 0x02, 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 d42685e704..d966d301f3 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 da645dcc9f..4450761c5d 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 b61abdec38..007c8448bf 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -71,7 +61,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\x7C") + build_motion_status_message(mock_device, "|") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) @@ -85,7 +75,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\x7E") + build_motion_status_message(mock_device, "~") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) @@ -99,7 +89,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\x58") + build_motion_status_message(mock_device, "X") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(70))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) @@ -113,7 +103,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\x5A") + build_motion_status_message(mock_device, "Z") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(70))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) @@ -127,7 +117,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\xBD") + build_motion_status_message(mock_device, "") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.lqi(50))) @@ -140,7 +130,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\xBF") + build_motion_status_message(mock_device, "") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.lqi(50))) @@ -153,7 +143,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "\x30") + build_motion_status_message(mock_device, "0") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) 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 5997a2f75c..5211ce1982 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 1113f34208..ddeaf77ce4 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 0e91d76d49..f0cd1053dc 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 614e971671..e368e852c8 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 25db204646..1f586bce88 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 f957f8ce2f..78634bbaa2 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..4f087c6e5d --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_third_reality_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("thirdreality.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("thirdreality") + end + end + return false +end + +return is_third_reality_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua new file mode 100644 index 0000000000..a5e92ef0b9 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { + { mfr = "Third Reality, Inc", model = "3RMS16BZ"}, + { mfr = "THIRDREALITY", model = "3RMS16BZ"} +} + +return ZIGBEE_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua index c93919f16b..37c6e5b8d6 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/thirdreality/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -20,19 +10,7 @@ local utils = require "st.utils" local APPLICATION_VERSION = "application_version" -local ZIGBEE_MOTION_SENSOR_FINGERPRINTS = { - { mfr = "Third Reality, Inc", model = "3RMS16BZ"}, - { mfr = "THIRDREALITY", model = "3RMS16BZ"} -} -local is_third_reality_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_MOTION_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local device_added = function(self, device) device:set_field(APPLICATION_VERSION, 0) @@ -73,7 +51,7 @@ local third_reality_motion_sensor = { lifecycle_handlers = { added = device_added, }, - can_handle = is_third_reality_motion_sensor + can_handle = require("thirdreality.can_handle"), } return third_reality_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..9b926e0845 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plugin_motion_sensor = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-plugin-motion-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("zigbee-plugin-motion-sensor") + end + end + return false +end + +return is_zigbee_plugin_motion_sensor diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua new file mode 100644 index 0000000000..459c3e7702 --- /dev/null +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS = { + { model = "E280-KR0A0Z0-HA" } +} + +return ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua index eb96b32a94..b387a35fbb 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/zigbee-plugin-motion-sensor/init.lua @@ -1,34 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" local OccupancySensing = zcl_clusters.OccupancySensing -local ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS = { - { model = "E280-KR0A0Z0-HA" } -} -local is_zigbee_plugin_motion_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_PLUGIN_MOTION_SENSOR_FINGERPRINTS) do - if device:get_model() == fingerprint.model then - return true - end - end - return false -end local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -59,7 +38,7 @@ local zigbee_plugin_motion_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = is_zigbee_plugin_motion_sensor + can_handle = require("zigbee-plugin-motion-sensor.can_handle"), } return zigbee_plugin_motion_sensor From 9bb8e3984d283e6a0b779b3cff45bf0ceeed8fb4 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:40:45 -0600 Subject: [PATCH 305/449] add handling for Power Topology cluster (#2444) --- .../light-energy-powerConsumption.yml | 16 + .../light-level-energy-powerConsumption.yml | 22 + .../profiles/light-level-power.yml | 20 + .../matter-switch/profiles/light-power.yml | 14 + .../src/embedded_clusters/Descriptor/init.lua | 53 +++ .../server/attributes/PartsList.lua | 78 ++++ .../Descriptor/server/attributes/init.lua | 27 ++ .../embedded_clusters/PowerTopology/init.lua | 55 +++ .../server/attributes/AvailableEndpoints.lua | 72 ++++ .../PowerTopology/server/attributes/init.lua | 23 + .../PowerTopology/types/Feature.lua | 32 ++ .../PowerTopology/types/init.lua | 14 + .../SmartThings/matter-switch/src/init.lua | 37 +- .../switch_handlers/attribute_handlers.lua | 113 +++-- .../src/switch_utils/device_configuration.lua | 73 ++-- .../matter-switch/src/switch_utils/fields.lua | 46 +- .../matter-switch/src/switch_utils/utils.lua | 153 +++++-- .../src/test/test_aqara_light_switch_h2.lua | 15 +- ...sor.lua => test_electrical_sensor_set.lua} | 100 +++-- .../src/test/test_electrical_sensor_tree.lua | 393 +++++++++++++++++ .../src/test/test_eve_energy.lua | 24 +- .../src/test/test_matter_multi_button.lua | 6 +- .../test_matter_multi_button_switch_mcd.lua | 52 +-- .../test/test_matter_switch_device_types.lua | 15 +- .../test_multi_switch_parent_child_lights.lua | 2 +- .../test_multi_switch_parent_child_plugs.lua | 408 +++++++++++++++--- 26 files changed, 1588 insertions(+), 275 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-level-power.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/light-power.yml create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua rename drivers/SmartThings/matter-switch/src/test/{test_electrical_sensor.lua => test_electrical_sensor_set.lua} (83%) create mode 100644 drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua diff --git a/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml new file mode 100644 index 0000000000..1f62938b9d --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-energy-powerConsumption.yml @@ -0,0 +1,16 @@ +name: light-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml new file mode 100644 index 0000000000..03963ccbd2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml @@ -0,0 +1,22 @@ +name: light-level-energy-powerConsumption +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml new file mode 100644 index 0000000000..23625ada16 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml @@ -0,0 +1,20 @@ +name: light-level-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/profiles/light-power.yml b/drivers/SmartThings/matter-switch/profiles/light-power.yml new file mode 100644 index 0000000000..27e07d0a49 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/light-power.yml @@ -0,0 +1,14 @@ +name: light-power +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: powerMeter + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua new file mode 100644 index 0000000000..fb7a38e63b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua @@ -0,0 +1,53 @@ +-- Copyright © 2025 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", + } + 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", +} + +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-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua new file mode 100644 index 0000000000..7c5b24d28d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2025 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-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua new file mode 100644 index 0000000000..8a3d9ea3c2 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2025 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-switch/src/embedded_clusters/PowerTopology/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua new file mode 100644 index 0000000000..0205e69b1f --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/init.lua @@ -0,0 +1,55 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local PowerTopologyServerAttributes = require "embedded_clusters.PowerTopology.server.attributes" +local PowerTopologyTypes = require "embedded_clusters.PowerTopology.types" + +local PowerTopology = {} + +PowerTopology.ID = 0x009C +PowerTopology.NAME = "PowerTopology" +PowerTopology.server = {} +PowerTopology.client = {} +PowerTopology.server.attributes = PowerTopologyServerAttributes:set_parent_cluster(PowerTopology) +PowerTopology.types = PowerTopologyTypes + +function PowerTopology:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0000] = "AvailableEndpoints", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +PowerTopology.attribute_direction_map = { + ["AvailableEndpoints"] = "server", +} + +PowerTopology.FeatureMap = PowerTopology.types.Feature + +function PowerTopology.are_features_supported(feature, feature_map) + if (PowerTopology.FeatureMap.bits_are_valid(feature)) then + return (feature & feature_map) == feature + end + return false +end + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = PowerTopology.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, PowerTopology.NAME)) + end + return PowerTopology[direction].attributes[key] +end +PowerTopology.attributes = {} +setmetatable(PowerTopology.attributes, attribute_helper_mt) + +setmetatable(PowerTopology, {__index = cluster_base}) + +return PowerTopology + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua new file mode 100644 index 0000000000..1536301d11 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/AvailableEndpoints.lua @@ -0,0 +1,72 @@ +-- Copyright © 2025 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 AvailableEndpoints = { + ID = 0x0000, + NAME = "AvailableEndpoints", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function AvailableEndpoints:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function AvailableEndpoints:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function AvailableEndpoints:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function AvailableEndpoints: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 AvailableEndpoints:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(AvailableEndpoints, {__call = AvailableEndpoints.new_value, __index = AvailableEndpoints.base_type}) +return AvailableEndpoints + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua new file mode 100644 index 0000000000..ff88697a0e --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/server/attributes/init.lua @@ -0,0 +1,23 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__index = function(self, key) + local req_loc = string.format("embedded_clusters.PowerTopology.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + return raw_def +end + +local PowerTopologyServerAttributes = {} + +function PowerTopologyServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(PowerTopologyServerAttributes, attr_mt) + +return PowerTopologyServerAttributes + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua new file mode 100644 index 0000000000..c2fe8087de --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/Feature.lua @@ -0,0 +1,32 @@ +-- Copyright © 2025 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.NODE_TOPOLOGY = 0x0001 +Feature.TREE_TOPOLOGY = 0x0002 +Feature.SET_TOPOLOGY = 0x0004 +Feature.DYNAMIC_POWER_FLOW = 0x0008 + +function Feature.bits_are_valid(feature) + local max = + Feature.NODE_TOPOLOGY | + Feature.TREE_TOPOLOGY | + Feature.SET_TOPOLOGY | + Feature.DYNAMIC_POWER_FLOW + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +setmetatable(Feature, new_mt) + +return Feature + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua new file mode 100644 index 0000000000..7609939803 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/PowerTopology/types/init.lua @@ -0,0 +1,14 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__index = function(self, key) + return require("embedded_clusters.PowerTopology.types." .. key) +end + +local PowerTopologyTypes = {} + +setmetatable(PowerTopologyTypes, types_mt) + +return PowerTopologyTypes + diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index ec3c5df9ba..51454b9675 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -22,9 +22,14 @@ local embedded_cluster_utils = require "switch_utils.embedded_cluster_utils" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + local SwitchLifecycleHandlers = {} function SwitchLifecycleHandlers.device_added(driver, device) @@ -32,6 +37,8 @@ function SwitchLifecycleHandlers.device_added(driver, device) -- was created after the initial subscription report if device.network_type == device_lib.NETWORK_TYPE_CHILD then device:send(clusters.OnOff.attributes.OnOff:read(device)) + elseif device.network_type == device_lib.NETWORK_TYPE_MATTER then + switch_utils.handle_electrical_sensor_info(device) end -- call device init in case init is not called after added due to device caching @@ -75,15 +82,8 @@ function SwitchLifecycleHandlers.device_init(driver, device) end local default_endpoint_id = switch_utils.find_default_endpoint(device) -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do + for _, ep in ipairs(device.endpoints) do if ep.endpoint_id ~= default_endpoint_id then - if device:supports_server_cluster(clusters.OnOff.ID, ep) then - local child_profile = switch_cfg.assign_child_profile(device, ep) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep, {persist = true}) - end - end local id = 0 for _, dt in ipairs(ep.device_types) do id = math.max(id, dt.device_type_id) @@ -103,14 +103,9 @@ function SwitchLifecycleHandlers.device_init(driver, device) -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. - local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - if #electrical_energy_measurement_eps > 0 then - local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( - device, - clusters.ElectricalEnergyMeasurement.ID, - {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} - ) - if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID, + {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then + device:set_field(fields.CUMULATIVE_REPORTS_SUPPORTED, true, {persist = false}) end end end @@ -137,9 +132,12 @@ local matter_driver_template = { [clusters.ColorControl.attributes.CurrentX.ID] = attribute_handlers.current_x_handler, [clusters.ColorControl.attributes.CurrentY.ID] = attribute_handlers.current_y_handler, }, + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.PartsList.ID] = attribute_handlers.parts_list_handler, + }, [clusters.ElectricalEnergyMeasurement.ID] = { - [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), - [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported.ID] = attribute_handlers.energy_imported_factory(false), + [clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported.ID] = attribute_handlers.energy_imported_factory(true), }, [clusters.ElectricalPowerMeasurement.ID] = { [clusters.ElectricalPowerMeasurement.attributes.ActivePower.ID] = attribute_handlers.active_power_handler, @@ -168,6 +166,9 @@ local matter_driver_template = { [clusters.PowerSource.attributes.BatChargeLevel.ID] = attribute_handlers.bat_charge_level_handler, [clusters.PowerSource.attributes.BatPercentRemaining.ID] = attribute_handlers.bat_percent_remaining_handler, }, + [clusters.PowerTopology.ID] = { + [clusters.PowerTopology.attributes.AvailableEndpoints.ID] = attribute_handlers.available_endpoints_handler, + }, [clusters.RelativeHumidityMeasurement.ID] = { [clusters.RelativeHumidityMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.relative_humidity_measured_value_handler }, 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 1ebd114812..40b1f6a44c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -9,6 +9,14 @@ local st_utils = require "st.utils" local fields = require "switch_utils.fields" local switch_utils = require "switch_utils.utils" local color_utils = require "switch_utils.color_utils" +local cfg = require "switch_utils.device_configuration" +local device_cfg = cfg.DeviceCfg + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end local AttributeHandlers = {} @@ -234,16 +242,11 @@ end function AttributeHandlers.active_power_handler(driver, device, ib, response) if ib.data.value then - local watt_value = ib.data.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.powerMeter.power({ value = watt_value, unit = "W"})) - end - if type(device.register_native_capability_attr_handler) == "function" then - device:register_native_capability_attr_handler("powerMeter","power") - end + local watt_value = ib.data.value / 1000 -- convert received milliwatt to watt + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.powerMeter.power({ value = watt_value, unit = "W"})) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("powerMeter","power") end end @@ -267,45 +270,87 @@ end -- [[ ELECTRICAL ENERGY MEASUREMENT CLUSTER ATTRIBUTES ]] -- -function AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - device:set_field(fields.TOTAL_IMPORTED_ENERGY, watt_hour_value, {persist = true}) - if ib.endpoint_id ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) +function AttributeHandlers.energy_imported_factory(is_periodic_report) + return function(driver, device, ib, response) + if version.api < 11 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct:augment_type(ib.data) + end + if ib.data.elements.energy then + local energy_imported_wh = ib.data.elements.energy.value / 1000 -- convert received milliwatt-hour to watt-hour + if is_periodic_report then + -- handle this report only if cumulative reports are not supported + if device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then return end + local energy_meter_latest_state = switch_utils.get_latest_state_for_endpoint( + device, ib, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME + ) or 0 + energy_imported_wh = energy_imported_wh + energy_meter_latest_state + end + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = energy_imported_wh, unit = "Wh" })) + switch_utils.report_power_consumption_to_st_energy(device, ib.endpoint_id, energy_imported_wh) else - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT), capabilities.energyMeter.energy({ value = watt_hour_value, unit = "Wh" })) + device.log.warn("Received data from the energy imported attribute does not include a numerical energy value") end - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) end end -function AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) - if ib.data.elements.energy then - local watt_hour_value = ib.data.elements.energy.value / fields.CONVERSION_CONST_MILLIWATT_TO_WATT - local latest_energy_report = device:get_field(fields.TOTAL_IMPORTED_ENERGY) or 0 - local summed_energy_report = latest_energy_report + watt_hour_value - device:set_field(fields.TOTAL_IMPORTED_ENERGY, summed_energy_report, {persist = true}) - device:emit_event(capabilities.energyMeter.energy({ value = summed_energy_report, unit = "Wh" })) - switch_utils.report_power_consumption_to_st_energy(device, device:get_field(fields.TOTAL_IMPORTED_ENERGY)) + +-- [[ POWER TOPOLOGY CLUSTER ATTRIBUTES ]] -- + +--- AvailableEndpoints: This attribute SHALL indicate the list of endpoints capable of +--- providing power to and/or consuming power from the endpoint hosting this server. +--- +--- In the case there are multiple endpoints supporting the PowerTopology cluster with +--- SET feature, all AvailableEndpoints responses must be handled before profiling. +function AttributeHandlers.available_endpoints_handler(driver, device, ib, response) + local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, set_ep_info in pairs(set_topology_eps or {}) do + if ib.endpoint_id == set_ep_info.endpoint_id then + -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) + local available_endpoints_ids = {} + for _, element in pairs(ib.data.elements) do + table.insert(available_endpoints_ids, element.value) + end + -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. + -- set an assigned child key in the case this will emit events on an EDGE_CHILD device + switch_utils.set_fields_for_electrical_sensor_endpoint(device, set_ep_info, available_endpoints_ids) + break + end + end + if #set_topology_eps == 0 then -- in other words, all AvailableEndpoints attribute responses have been handled + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) end end -function AttributeHandlers.energy_imported_factory(is_cumulative_report) - return function(driver, device, ib, response) - if is_cumulative_report then - AttributeHandlers.cumul_energy_imported_handler(driver, device, ib, response) - elseif device:get_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED) then - AttributeHandlers.per_energy_imported_handler(driver, device, ib, response) + +-- [[ DESCRIPTOR CLUSTER ATTRIBUTES ]] -- + +function AttributeHandlers.parts_list_handler(driver, device, ib, response) + local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) + for i, tree_ep_info in pairs(tree_topology_eps or {}) do + if ib.endpoint_id == tree_ep_info.endpoint_id then + -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) + local associated_endpoints_ids = {} + for _, element in pairs(ib.data.elements) do + table.insert(associated_endpoints_ids, element.value) + end + -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. + -- set an assigned child key in the case this will emit events on an EDGE_CHILD device + switch_utils.set_fields_for_electrical_sensor_endpoint(device, tree_ep_info, associated_endpoints_ids) + break end end + if #tree_topology_eps == 0 then -- in other words, all PartsList attribute responses for TREE Electrical Sensor EPs have been handled + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true}) + device_cfg.match_profile(driver, device) + end end -- [[ POWER SOURCE CLUSTER ATTRIBUTES ]] -- - function AttributeHandlers.bat_percent_remaining_handler(driver, device, ib, response) if ib.data.value then device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) 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 eebe734fcd..4786f7b7fb 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -29,10 +29,13 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on local generic_profile = fields.device_type_profile_map[primary_dt_id] - if is_child_device and ( - server_onoff_ep_id == switch_utils.get_product_override_field(device, "ep_id") or - generic_profile == switch_utils.get_product_override_field(device, "initial_profile") - ) then + local static_electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, server_onoff_ep_id) + if static_electrical_tags ~= nil then + -- 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 + + if is_child_device and generic_profile == switch_utils.get_product_override_field(device, "initial_profile") then generic_profile = switch_utils.get_product_override_field(device, "target_profile") or generic_profile end @@ -45,25 +48,21 @@ function SwitchDeviceConfiguration.create_child_devices(driver, device, server_o return end - local device_num = 0 table.sort(server_onoff_ep_ids) - for idx, ep_id in ipairs(server_onoff_ep_ids) do - device_num = device_num + 1 + for device_num, ep_id in ipairs(server_onoff_ep_ids) do if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint - local name = string.format("%s %d", device.label, device_num) + local label_and_name = string.format("%s %d", device.label, device_num) local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) - driver:try_create_device({ - type = "EDGE_CHILD", - label = name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep_id), - vendor_provided_label = name - }) - if idx == 1 and string.find(child_profile, "energy") then - -- when energy management is defined in the root endpoint(0), replace it with the first switch endpoint and process it. - device:set_field(fields.ENERGY_MANAGEMENT_ENDPOINT, ep_id, {persist = true}) - end + driver:try_create_device( + { + type = "EDGE_CHILD", + label = label_and_name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = label_and_name + } + ) end end @@ -74,15 +73,15 @@ end -- Per the spec, these attributes are "meant to be changed only during commissioning." function SwitchDeviceConfiguration.set_device_control_options(device) - for _, ep_info in ipairs(device.endpoints) do + for _, ep in ipairs(device.endpoints) do -- before the Matter 1.3 lua libs update (HUB FW 54), OptionsBitmap was defined as LevelControlOptions - if switch_utils.ep_supports_cluster(ep_info, clusters.LevelControl.ID) then - device:send(clusters.LevelControl.attributes.Options:write(device, ep_info.endpoint_id, clusters.LevelControl.types.LevelControlOptions.EXECUTE_IF_OFF)) + if switch_utils.find_cluster_on_ep(ep, clusters.LevelControl.ID) then + device:send(clusters.LevelControl.attributes.Options:write(device, ep.endpoint_id, clusters.LevelControl.types.LevelControlOptions.EXECUTE_IF_OFF)) end -- before the Matter 1.4 lua libs update (HUB FW 56), there was no OptionsBitmap type defined - if switch_utils.ep_supports_cluster(ep_info, clusters.ColorControl.ID) then + if switch_utils.find_cluster_on_ep(ep, clusters.ColorControl.ID) then local excute_if_off_bit = clusters.ColorControl.types.OptionsBitmap and clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF or 0x0001 - device:send(clusters.ColorControl.attributes.Options:write(device, ep_info.endpoint_id, excute_if_off_bit)) + device:send(clusters.ColorControl.attributes.Options:write(device, ep.endpoint_id, excute_if_off_bit)) end end end @@ -160,9 +159,20 @@ end -- [[ PROFILE MATCHING AND CONFIGURATIONS ]] -- +local function profiling_data_still_required(device) + for _, field in pairs(fields.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 + function DeviceConfiguration.match_profile(driver, device) + if profiling_data_still_required(device) then return end + local default_endpoint_id = switch_utils.find_default_endpoint(device) - local updated_profile = nil + local updated_profile if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then updated_profile = "water-valve" @@ -180,16 +190,7 @@ function DeviceConfiguration.match_profile(driver, device) if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end - if generic_profile("plug-binary") or generic_profile("plug-level") then - if switch_utils.check_switch_category_vendor_overrides(device) then - updated_profile = string.gsub(updated_profile, "plug", "switch") - else - local electrical_tags = "" - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalPowerMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-power" end - if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) > 0 then electrical_tags = electrical_tags .. "-energy-powerConsumption" end - if electrical_tags ~= "" then updated_profile = string.gsub(updated_profile, "-binary", "") .. electrical_tags end - end - elseif generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then + if generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then updated_profile = "light-color-level-fan" elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then updated_profile = "light-level-motion" diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 3a0bdc9fdb..56ac1e5366 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -73,18 +73,20 @@ SwitchFields.device_type_profile_map = { [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", } - -SwitchFields.CONVERSION_CONST_MILLIWATT_TO_WATT = 1000 -- A milliwatt is 1/1000th of a watt - - -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for -- devices that were joined to this driver as MCD devices before the transition -- to join switch devices as parent-child. This value will exist in the device -- table for devices that joined prior to this transition, and is also used for -- button devices that require component mapping. SwitchFields.COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -SwitchFields.ENERGY_MANAGEMENT_ENDPOINT = "__energy_management_endpoint" SwitchFields.IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" + +--- If the ASSIGNED_CHILD_KEY field is populated for an endpoint, it should be +--- used as the key in the get_child_by_parent_assigned_key() function. This allows +--- multiple endpoints to associate with the same child device, though right now child +--- devices are keyed using only one endpoint id. +SwitchFields.ASSIGNED_CHILD_KEY = "__assigned_child_key" + SwitchFields.COLOR_TEMP_BOUND_RECEIVED_KELVIN = "__colorTemp_bound_received_kelvin" SwitchFields.COLOR_TEMP_BOUND_RECEIVED_MIRED = "__colorTemp_bound_received_mired" SwitchFields.COLOR_TEMP_MIN = "__color_temp_min" @@ -96,26 +98,19 @@ SwitchFields.COLOR_MODE = "__color_mode" SwitchFields.updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, - { current_field_name = "__switch_intialized", updated_field_name = nil } + { current_field_name = "__switch_intialized", updated_field_name = nil }, + { current_field_name = "__energy_management_endpoint", updated_field_name = nil }, + { current_field_name = "__total_imported_energy", updated_field_name = nil }, } -SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION -SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - - SwitchFields.vendor_overrides = { - [0x1321] = { + [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, [0x115F] = { -- AQARA_MANUFACTURER_ID - [0x1003] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - [0x1004] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 2 Channels(On/Off Light) - [0x1005] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 3 Channels(On/Off Light) - [0x1008] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 2 Buttons(Generic Switch), 1 Channel(On/Off Light) - [0x1009] = { target_profile = "light-power-energy-powerConsumption", ep_id = 1 }, -- 4 Buttons(Generic Switch), 2 Channels(On/Off Light) - [0x1006] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 3 Buttons(Generic Switch), 1 Channels(Dimmable Light) - [0x100A] = { ignore_combo_switch_button = true, target_profile = "light-level-power-energy-powerConsumption", ep_id = 1 }, -- 1 Buttons(Generic Switch), 1 Channels(Dimmable Light) + [0x1006] = { ignore_combo_switch_button = true }, -- 3 Buttons(Generic Switch), 1 Channel (Dimmable Light) + [0x100A] = { ignore_combo_switch_button = true }, -- 1 Buttons(Generic Switch), 1 Channel (Dimmable Light) [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile } } @@ -145,8 +140,19 @@ SwitchFields.switch_category_vendor_overrides = { {0xEEE2, 0xAB08, 0xAB31, 0xAB04, 0xAB01, 0xAB43, 0xAB02, 0xAB03, 0xAB05} } -SwitchFields.CUMULATIVE_REPORTS_NOT_SUPPORTED = "__cumulative_reports_not_supported" -SwitchFields.TOTAL_IMPORTED_ENERGY = "__total_imported_energy" +--- stores a table of endpoints that support the Electrical Sensor device type, used during profiling +--- in AvailableEndpoints and PartsList handlers for SET and TREE PowerTopology features, respectively +SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" + +--- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc. +--- for an Electrical Sensor EP with a "primary" endpoint, used during device profling. +SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" + +SwitchFields.profiling_data = { + POWER_TOPOLOGY = "__power_topology", +} + +SwitchFields.CUMULATIVE_REPORTS_SUPPORTED = "__cumulative_reports_supported" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index b6cc09e007..1e61f60c74 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -4,10 +4,22 @@ local MatterDriver = require "st.matter.driver" local fields = require "switch_utils.fields" local st_utils = require "st.utils" +local version = require "version" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" +local im = require "st.matter.interaction_model" local log = require "log" -local version = require "version" + +-- Include driver-side definitions when lua libs api version is < 11 +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end local utils = {} @@ -33,6 +45,14 @@ function utils.set_field_for_endpoint(device, field, endpoint, value, additional device:set_field(string.format("%s_%d", field, endpoint), value, additional_params) end +function utils.remove_field_index(device, field_name, index) + local new_table = device:get_field(field_name) + if type(new_table) == "table" then + new_table[index] = nil -- remove value associated with index from table + device:set_field(field_name, new_table) + end +end + function utils.mired_to_kelvin(value, minOrMax) if value == 0 then -- shouldn't happen, but has value = 1 @@ -225,8 +245,9 @@ function utils.emit_event_for_endpoint(device, ep_info, event) device:emit_component_event(comp, event) end -function utils.find_child(parent, ep_id) - return parent:get_child_by_parent_assigned_key(string.format("%d", ep_id)) +function utils.find_child(parent_device, ep_id) + local assigned_key = utils.get_field_for_endpoint(parent_device, fields.ASSIGNED_CHILD_KEY, ep_id) or ep_id + return parent_device:get_child_by_parent_assigned_key(string.format("%d", assigned_key)) end function utils.get_endpoint_info(device, endpoint_id) @@ -236,18 +257,18 @@ function utils.get_endpoint_info(device, endpoint_id) return {} end -function utils.ep_supports_cluster(ep_info, cluster_id, opts) +function utils.find_cluster_on_ep(ep, cluster_id, opts) opts = opts or {} local clus_has_features = function(cluster, checked_feature) return (cluster.feature_map & checked_feature) == checked_feature end - for _, cluster in ipairs(ep_info.clusters) do + for _, cluster in ipairs(ep.clusters) do if ((cluster.cluster_id == cluster_id) and (opts.feature_bitmap == nil or clus_has_features(cluster, opts.feature_bitmap)) and ((opts.cluster_type == nil and cluster.cluster_type == "SERVER" or cluster.cluster_type == "BOTH") or (opts.cluster_type == cluster.cluster_type)) or (cluster_id == nil)) then - return true + return cluster end end end @@ -258,12 +279,18 @@ function utils.matter_handler(driver, device, response_block) end -- get a list of endpoints for a specified device type. -function utils.get_endpoints_by_device_type(device, device_type_id) +function utils.get_endpoints_by_device_type(device, device_type_id, opts) + opts = opts or {} local dt_eps = {} for _, ep in ipairs(device.endpoints) do for _, dt in ipairs(ep.device_types) do if dt.device_type_id == device_type_id then - table.insert(dt_eps, ep.endpoint_id) + if opts.with_info then + table.insert(dt_eps, ep) + else + table.insert(dt_eps, ep.endpoint_id) + end + break end end end @@ -285,16 +312,22 @@ function utils.detect_bridge(device) return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end -function utils.detect_matter_thing(device) - for _, capability in ipairs(fields.supported_capabilities) do - if device:supports_capability(capability) then - return false - end +--- Generalizes the 'get_latest_state' function to be callable with extra endpoint information, described below, +--- without directly specifying the expected component. See the 'get_latest_state' definition for more +--- information about parameters and expected functionality otherwise. +--- +--- @param endpoint_info number|table an endpoint id or an ib (the ib data includes endpoint_id, cluster_id, and attribute_id fields) +function utils.get_latest_state_for_endpoint(device, endpoint_info, capability_id, attribute_id, default_value, default_state_table) + if type(endpoint_info) == "number" then + endpoint_info = { endpoint_id = endpoint_info } end - return device:supports_capability(capabilities.refresh) + + local component = device:endpoint_to_component(endpoint_info) + local state_device = utils.find_child(device, endpoint_info.endpoint_id) or device + return state_device:get_latest_state(component, capability_id, attribute_id, default_value, default_state_table) end -function utils.report_power_consumption_to_st_energy(device, latest_total_imported_energy_wh) +function utils.report_power_consumption_to_st_energy(device, endpoint_id, total_imported_energy_wh) local current_time = os.time() local last_time = device:get_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP) or 0 @@ -302,34 +335,78 @@ function utils.report_power_consumption_to_st_energy(device, latest_total_import if fields.MINIMUM_ST_ENERGY_REPORT_INTERVAL >= (current_time - last_time) then return end - device:set_field(fields.LAST_IMPORTED_REPORT_TIMESTAMP, current_time, { persist = true }) - -- Calculate the energy delta between reports - local energy_delta_wh = 0.0 - local previous_imported_report = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) - if previous_imported_report and previous_imported_report.energy then - energy_delta_wh = math.max(latest_total_imported_energy_wh - previous_imported_report.energy, 0.0) - end - + local previous_imported_report = utils.get_latest_state_for_endpoint(device, endpoint_id, capabilities.powerConsumptionReport.ID, + capabilities.powerConsumptionReport.powerConsumption.NAME, { energy = total_imported_energy_wh }) -- default value if nil + -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' local epoch_to_iso8601 = function(time) return os.date("!%Y-%m-%dT%H:%M:%SZ", time) end -- Return an ISO-8061 timestamp from UTC + device:emit_event_for_endpoint(endpoint_id, capabilities.powerConsumptionReport.powerConsumption({ + start = epoch_to_iso8601(last_time), + ["end"] = epoch_to_iso8601(current_time - 1), + deltaEnergy = total_imported_energy_wh - previous_imported_report.energy, + energy = total_imported_energy_wh + })) +end - -- Report the energy consumed during the time interval. The unit of these values should be 'Wh' - if not device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT) then - device:emit_event(capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) +--- sets fields for handling EPs with the Electrical Sensor device type +--- +--- @param device table a Matter device object +--- @param electrical_sensor_ep table an EP object that includes an Electrical Sensor device type +--- @param associated_endpoint_ids table EP IDs that are associated with the Electrical Sensor EP +--- @return boolean +function utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_ep, associated_endpoint_ids) + if #associated_endpoint_ids == 0 then + return false else - device:emit_event_for_endpoint(device:get_field(fields.ENERGY_MANAGEMENT_ENDPOINT),capabilities.powerConsumptionReport.powerConsumption({ - start = epoch_to_iso8601(last_time), - ["end"] = epoch_to_iso8601(current_time - 1), - deltaEnergy = energy_delta_wh, - energy = latest_total_imported_energy_wh - })) + local tags = "" + if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalPowerMeasurement.ID) then tags = tags.."-power" end + if utils.find_cluster_on_ep(electrical_sensor_ep, clusters.ElectricalEnergyMeasurement.ID) then tags = tags.."-energy-powerConsumption" end + -- note: using the lowest valued EP ID here is arbitrary (not spec defined) and is done to create internal consistency + -- Ex. for the NODE topology, electrical capabilities will then be associated with the default (aka lowest ID'd) OnOff EP + table.sort(associated_endpoint_ids) + local primary_associated_ep_id = associated_endpoint_ids[1] + -- map the required electrical tags for this electrical sensor EP with the first associated EP ID, used later during profling. + utils.set_field_for_endpoint(device, fields.ELECTRICAL_TAGS, primary_associated_ep_id, tags) + utils.set_field_for_endpoint(device, fields.ASSIGNED_CHILD_KEY, electrical_sensor_ep.endpoint_id, string.format("%d", primary_associated_ep_id), { persist = true }) + return true + end +end + +function utils.handle_electrical_sensor_info(device) + local electrical_sensor_eps = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR, { with_info = true }) + if #electrical_sensor_eps == 0 then + -- no Electrical Sensor EPs are supported. Set profiling data to false and return + device:set_field(fields.profiling_data.POWER_TOPOLOGY, false, {persist=true}) + return + end + + -- check the feature map for the first (or only) Electrical Sensor EP + local endpoint_power_topology_cluster = utils.find_cluster_on_ep(electrical_sensor_eps[1], clusters.PowerTopology.ID) or {} + local endpoint_power_topology_feature_map = endpoint_power_topology_cluster.feature_map or 0 + if clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.SET_TOPOLOGY, endpoint_power_topology_feature_map) then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a SET topology + local available_eps_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) -- SET read + for _, ep in ipairs(electrical_sensor_eps) do + available_eps_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(device, ep.endpoint_id)) + end + device:send(available_eps_req) + return + elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, endpoint_power_topology_feature_map) then + device:set_field(fields.ELECTRICAL_SENSOR_EPS, electrical_sensor_eps) -- assume any other stored EPs also have a TREE topology + local parts_list_req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) -- TREE read + for _, ep in ipairs(electrical_sensor_eps) do + parts_list_req:merge(clusters.Descriptor.attributes.PartsList:read(device, ep.endpoint_id)) + end + device:send(parts_list_req) + return + elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, endpoint_power_topology_feature_map) then + -- EP has a NODE topology, so there is only ONE Electrical Sensor EP + device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, {persist=true}) + if utils.set_fields_for_electrical_sensor_endpoint(device, electrical_sensor_eps[1], device:get_endpoints(clusters.OnOff.ID)) == false then + device.log.warn("Electrical Sensor EP with NODE topology found, but no OnOff EPs exist. Electrical Sensor capabilities will not be exposed.") + end + return end 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 ec91ddba9f..45d478e43d 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 @@ -31,7 +31,8 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ clusters = { {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, {cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 2 }, - {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 } + {cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 5 }, + {cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1 } -- NODE_TOPOLOGY }, device_types = { {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode @@ -273,10 +274,8 @@ test.register_coroutine_test( function() test.socket.matter:__queue_receive( { - -- don't use "aqara_mock_children[aqara_child1_ep].id," - -- because energy management is at the root endpoint. aqara_mock_device.id, - clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 1, 17000) + clusters.ElectricalPowerMeasurement.attributes.ActivePower:build_test_report_data(aqara_mock_device, 0, 17000) } ) @@ -290,7 +289,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_19) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_19) } ) @@ -310,7 +309,7 @@ test.register_coroutine_test( test.socket.matter:__queue_receive( { aqara_mock_device.id, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 1, cumulative_report_val_29) + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data(aqara_mock_device, 0, cumulative_report_val_29) } ) @@ -327,7 +326,7 @@ test.register_coroutine_test( { aqara_mock_device.id, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported:build_test_report_data( - aqara_mock_device, 1, cumulative_report_val_39 + aqara_mock_device, 0, cumulative_report_val_39 ) } ) @@ -340,7 +339,7 @@ test.register_coroutine_test( aqara_mock_children[aqara_child1_ep]:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ start = "1970-01-01T00:15:01Z", ["end"] = "1970-01-01T00:40:00Z", - deltaEnergy = 0.0, + deltaEnergy = 20.0, energy = 39.0 })) ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua similarity index 83% rename from drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua rename to drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 21286fc55f..3df62056ca 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -5,11 +5,13 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" local version = require "version" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" end local mock_device = test.mock_device.build_test_matter_device({ @@ -33,6 +35,7 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters = { { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY }, device_types = { { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor @@ -47,7 +50,27 @@ local mock_device = test.mock_device.build_test_matter_device({ device_types = { { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit } - } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- Dimmable Plug In Unit + } + }, }, }) @@ -71,28 +94,22 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 10, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, } -- SET_TOPOLOGY }, device_types = { - { device_type_id = 0x0510, device_type_revision = 1 } -- Electrical Sensor + { device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor } }, - { - endpoint_id = 2, - clusters = { - { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, - }, - device_types = { - { device_type_id = 0x010A, device_type_revision = 1 }, -- On Off Plug In Unit - } - } }, }) local subscribed_attributes_periodic = { clusters.OnOff.attributes.OnOff, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local subscribed_attributes = { clusters.OnOff.attributes.OnOff, @@ -145,14 +162,19 @@ local periodic_report_val_23 = { } local function test_init() + test.mock_device.add_test_device(mock_device) local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 1) + read_req:merge(clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) 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) @@ -164,11 +186,13 @@ local function test_init_periodic() subscribe_request:merge(cluster:subscribe(mock_device_periodic)) end end - test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "added" }) + local read_req = clusters.PowerTopology.attributes.AvailableEndpoints:read(mock_device_periodic.id, 1) + test.socket.matter:__expect_send({ mock_device_periodic.id, read_req }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "init" }) test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request }) end test.register_message_test( @@ -323,26 +347,26 @@ test.register_coroutine_test( end ) -test.register_message_test( +test.register_coroutine_test( "Periodic Energy as subordinate to Cumulative Energy measurement should not generate any messages", - { - { - channel = "matter", - direction = "receive", - message = { + function() + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) } - }, - { - channel = "matter", - direction = "receive", - message = { + ) + test.socket.matter:__queue_receive( + { mock_device.id, - clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data(mock_device, 1, periodic_report_val_23) + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device, 1, periodic_report_val_23 + ) } - }, - } + ) + end ) test.register_coroutine_test( @@ -407,10 +431,22 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) end, { test_init = test_init } ) @@ -419,8 +455,10 @@ test.register_coroutine_test( "Test profile change on init for only Periodic Electrical Sensor device type", function() test.socket.device_lifecycle:__queue_receive({ mock_device_periodic.id, "doConfigure" }) - mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) mock_device_periodic:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device_periodic.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device_periodic, 1, {uint32(1)})}) + mock_device_periodic:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) end, { test_init = test_init_periodic } ) 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 new file mode 100644 index 0000000000..a16d7372b5 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -0,0 +1,393 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local uint32 = require "st.matter.data_types.Uint32" +local version = require "version" + +if version.api < 11 then + clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" + clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" + clusters.PowerTopology = require "embedded_clusters.PowerTopology" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +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 = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + 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.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 0, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 2, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + { + endpoint_id = 3, + clusters = { + { cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 14, }, + { cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 2, }, -- TREE_TOPOLOGY + }, + device_types = { + { device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor + } + }, + { + endpoint_id = 4, + clusters = { + { cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 0, }, + { cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + }, + device_types = { + { device_type_id = 0x010B, device_type_revision = 1 }, -- OnOff Dimmable Plug + } + }, + }, +}) + +local subscribed_attributes = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ElectricalPowerMeasurement.attributes.ActivePower, + clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, + clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, +} + +local cumulative_report_val_19 = { + energy = 19000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_29 = { + energy = 29000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local cumulative_report_val_39 = { + energy = 39000, + start_timestamp = 0, + end_timestamp = 0, + start_systime = 0, + end_systime = 0, + apparent_energy = 0, + reactive_energy = 0 +} + +local function test_init() + test.mock_device.add_test_device(mock_device) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) + for i, cluster in ipairs(subscribed_attributes) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + local read_req = clusters.Descriptor.attributes.PartsList:read(mock_device.id, 1) + read_req:merge(clusters.Descriptor.attributes.PartsList:read(mock_device.id, 3)) + test.socket.matter:__expect_send({ mock_device.id, read_req }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) + test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "On command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Off command should send the appropriate commands", + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "off" } + } + }, + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "off", args = { } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 2) + } + } + } +) + +test.register_message_test( + "Active power measurement should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ElectricalPowerMeasurement.server.attributes.ActivePower:build_test_report_data(mock_device, 1, 17000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerMeter.power({value = 17.0, unit="W"})) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "powerMeter", capability_attr_id = "power" } + } + } + } +) + +test.register_coroutine_test( + "Cumulative Energy measurement should generate correct messages", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_29 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 29.0, unit = "Wh" })) + ) + + test.wait_for_events() + test.mock_time.advance_time(1500) + + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 39.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 20.0, + energy = 39.0 + })) + ) + end +) + +test.register_coroutine_test( + "Test profile change on init for Electrical Sensor device type", + function() + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.Descriptor.attributes.PartsList:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + end, + { test_init = test_init } +) + +test.register_message_test( + "Set level command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switchLevel", component = "main", command = "setLevel", args = {20,20} } + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_cmd_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_cmd_id = "setLevel" } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, math.floor(20/100.0 * 254), 20, 0 ,0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff:build_test_command_response(mock_device, 2) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 2, 50) + } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(20)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 2, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } + } + }, + } +) + +test.run_registered_tests() 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 f0ab1b0e9b..1598f953a4 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -98,6 +98,12 @@ local mock_device_electrical_sensor = test.mock_device.build_test_matter_device( cluster_type = "SERVER", cluster_revision = 1, feature_map = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY, + }, + { + cluster_id = clusters.PowerTopology.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.PowerTopology.types.Feature.NODE_TOPOLOGY, } }, device_types = { @@ -108,17 +114,27 @@ local mock_device_electrical_sensor = test.mock_device.build_test_matter_device( }) local function test_init_electrical_sensor() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_electrical_sensor) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_electrical_sensor) - for i, cluster in ipairs(cluster_subscribe_list) do - subscribe_request:merge(cluster:subscribe(mock_device_electrical_sensor)) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_electrical_sensor)) end end - test.socket.matter:__expect_send({ mock_device_electrical_sensor.id, subscribe_request }) - test.mock_device.add_test_device(mock_device_electrical_sensor) + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "added" }) + test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "init" }) + test.socket.matter:__expect_send({mock_device_electrical_sensor.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device_electrical_sensor.id, "doConfigure" }) + mock_device_electrical_sensor:expect_metadata_update({ profile = "plug-energy-powerConsumption" }) + mock_device_electrical_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init() 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 e1cde750f4..9f79c3c85f 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 @@ -13,13 +13,15 @@ local button_attr = capabilities.button.button -- Mock a 5-button device using endpoints non-consecutive endpoints local mock_device = test.mock_device.build_test_matter_device( { - profile = t_utils.get_profile_definition("5-button-battery.yml"), -- on a real device we would switch to this, rather than fingerprint to it + profile = t_utils.get_profile_definition("5-button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, endpoints = { { endpoint_id = 0, clusters = {}, - device_types = {} + device_types = { + {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode + } }, { endpoint_id = 10, 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 0eb10ba6f7..a036a79f18 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 @@ -417,40 +417,44 @@ test.register_coroutine_test( clusters.Switch.server.events.ShortRelease, clusters.Switch.server.events.MultiPressComplete, } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_mcd_unsupported_switch_device_type) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_mcd_unsupported_switch_device_type)) - end + local unsup_mock_device = mock_device_mcd_unsupported_switch_device_type + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) + for _, cluster in ipairs(cluster_subscribe_list) do + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "added" }) - test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - -- init results in subscription interaction - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "init" }) - test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "init" }) + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) - -- doConfigure sets the provisioning state to provisioned - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ profile = "2-button" }) - mock_device_mcd_unsupported_switch_device_type:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - mock_device_mcd_unsupported_switch_device_type:expect_device_create({ + test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "doConfigure" }) + unsup_mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 1", profile = "switch-binary", - parent_device_id = mock_device_mcd_unsupported_switch_device_type.id, + parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", 7) }) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "doConfigure" }) + unsup_mock_device:expect_metadata_update({ profile = "2-button" }) + unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.wait_for_events() - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device_mcd_unsupported_switch_device_type.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device_mcd_unsupported_switch_device_type.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device_mcd_unsupported_switch_device_type.id, subscribe_request}) + local updated_device_profile = t_utils.get_profile_definition("2-button.yml") + test.socket.device_lifecycle:__queue_receive(unsup_mock_device:generate_info_changed({ profile = updated_device_profile })) + local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(unsup_mock_device)) + end + end + test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) + + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) end, { test_init = test_init_mcd_unsupported_switch_device_type } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 920b9a4b5f..272ea3dad1 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -385,12 +385,7 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device { endpoint_id = 1, clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0, --u32 bitmap - }, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} }, device_types = { @@ -447,6 +442,10 @@ end local function test_init_onoff_client() test.mock_device.add_test_device(mock_device_onoff_client) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_onoff_client.id, "doConfigure" }) + mock_device_onoff_client:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end local function test_init_parent_client_child_server() @@ -466,6 +465,8 @@ end local function test_init_dimmer() test.mock_device.add_test_device(mock_device_dimmer) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "init" }) test.socket.device_lifecycle:__queue_receive({ mock_device_dimmer.id, "doConfigure" }) test.socket.matter:__expect_send({ mock_device_dimmer.id, @@ -732,4 +733,4 @@ test.register_coroutine_test( { test_init = test_init_light_level_motion } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 178a5bf37d..4a5787346f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -687,4 +687,4 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index cc076fd35e..215c8f34ed 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -8,15 +8,17 @@ local clusters = require "st.matter.clusters" test.disable_startup_messages() -local child_profile = t_utils.get_profile_definition("plug-binary.yml") -local child_profile_override = t_utils.get_profile_definition("switch-binary.yml") +local TRANSITION_TIME = 0 +local OPTIONS_MASK = 0x01 +local OPTIONS_OVERRIDE = 0x01 + local parent_ep = 10 local child1_ep = 20 local child2_ep = 30 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("plug-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -37,36 +39,44 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { endpoint_id = child1_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { endpoint_id = child2_ep, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light } }, } }) -local mock_device_child_profile_override = test.mock_device.build_test_matter_device({ +local child1_ep_non_sequential = 50 +local child2_ep_non_sequential = 30 +local child3_ep_non_sequential = 40 + +local mock_device_parent_child_endpoints_non_sequential = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("switch-binary.yml"), + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), manufacturer_info = { vendor_id = 0x1321, - product_id = 0x000D, + product_id = 0x000C, }, endpoints = { { @@ -84,20 +94,33 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2} -- On/Off Light } }, { - endpoint_id = child1_ep, + endpoint_id = child1_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2} }, device_types = { - {device_type_id = 0x010A, device_type_revision = 2} -- On/Off Plug + {device_type_id = 0x0100, device_type_revision = 2}, -- On/Off Light + {device_type_id = 0x0101, device_type_revision = 2} -- Dimmable Light } }, { - endpoint_id = child2_ep, + endpoint_id = child2_ep_non_sequential, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + }, + device_types = { + {device_type_id = 0x010D, device_type_revision = 2} -- Extended Color Light + } + }, + { + endpoint_id = child3_ep_non_sequential, clusters = { {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, }, @@ -108,11 +131,16 @@ local mock_device_child_profile_override = test.mock_device.build_test_matter_de } }) +local child_profiles = { + [child1_ep] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep] = t_utils.get_profile_definition("light-color-level.yml"), +} + local mock_children = {} for i, endpoint in ipairs(mock_device.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile, + profile = child_profiles[endpoint.endpoint_id], device_network_id = string.format("%s:%d", mock_device.id, endpoint.endpoint_id), parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) @@ -125,8 +153,23 @@ local function test_init() test.mock_device.add_test_device(mock_device) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -135,7 +178,10 @@ local function test_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 = "plug-binary" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child1_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, child2_ep, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, child2_ep, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ profile = "light-binary" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children) do @@ -145,7 +191,7 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "plug-binary", + profile = "light-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child1_ep) }) @@ -153,60 +199,93 @@ local function test_init() mock_device:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", - profile = "plug-binary", + profile = "light-color-level", parent_device_id = mock_device.id, parent_assigned_child_key = string.format("%d", child2_ep) }) end -local mock_children_child_profile_override = {} -for i, endpoint in ipairs(mock_device_child_profile_override.endpoints) do +local child_profiles_non_sequential = { + [child1_ep_non_sequential] = t_utils.get_profile_definition("light-level.yml"), + [child2_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), + [child3_ep_non_sequential] = t_utils.get_profile_definition("light-color-level.yml"), +} + +local mock_children_non_sequential = {} +for i, endpoint in ipairs(mock_device_parent_child_endpoints_non_sequential.endpoints) do if endpoint.endpoint_id ~= parent_ep and endpoint.endpoint_id ~= 0 then local child_data = { - profile = child_profile_override, - device_network_id = string.format("%s:%d", mock_device_child_profile_override.id, endpoint.endpoint_id), - parent_device_id = mock_device_child_profile_override.id, + profile = child_profiles_non_sequential[endpoint.endpoint_id], + device_network_id = string.format("%s:%d", mock_device_parent_child_endpoints_non_sequential.id, endpoint.endpoint_id), + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, parent_assigned_child_key = string.format("%d", endpoint.endpoint_id) } - mock_children_child_profile_override[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) + mock_children_non_sequential[endpoint.endpoint_id] = test.mock_device.build_test_child_device(child_data) end end -local function test_init_child_profile_override() - test.mock_device.add_test_device(mock_device_child_profile_override) +local function test_init_parent_child_endpoints_non_sequential() + test.mock_device.add_test_device(mock_device_parent_child_endpoints_non_sequential) local cluster_subscribe_list = { clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_child_profile_override) + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_parent_child_endpoints_non_sequential)) + end + end - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "added" }) - test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "init" }) - test.socket.matter:__expect_send({mock_device_child_profile_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "init" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_child_profile_override.id, "doConfigure" }) - mock_device_child_profile_override:expect_metadata_update({ profile = "switch-binary" }) - mock_device_child_profile_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_endpoints_non_sequential.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.ColorControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - for _, child in pairs(mock_children_child_profile_override) do + for _, child in pairs(mock_children_non_sequential) do test.mock_device.add_test_device(child) end - mock_device:expect_device_create({ + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 2", - profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child1_ep) + profile = "light-color-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child2_ep_non_sequential) }) - mock_device:expect_device_create({ + -- switch-binary will be selected as an overridden child device profile + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ type = "EDGE_CHILD", label = "Matter Switch 3", profile = "switch-binary", - parent_device_id = mock_device_child_profile_override.id, - parent_assigned_child_key = string.format("%d", child2_ep) + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child3_ep_non_sequential) + }) + + mock_device_parent_child_endpoints_non_sequential:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Switch 4", + profile = "light-level", + parent_device_id = mock_device_parent_child_endpoints_non_sequential.id, + parent_assigned_child_key = string.format("%d", child1_ep_non_sequential) }) end @@ -365,19 +444,244 @@ test.register_message_test( } ) -test.register_coroutine_test( - "Added should call refresh for child devices", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_children[child1_ep].id, "added" }) - local req = clusters.OnOff.attributes.OnOff:read(mock_children[child1_ep]) - test.socket.matter:__expect_send({mock_device.id, req}) - end +test.register_message_test( + "Current level reports should generate appropriate events", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.server.attributes.CurrentLevel:build_test_report_data(mock_device, child1_ep, 50) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "switchLevel", capability_attr_id = "level" } + } + }, + } +) + +test.register_message_test( + "Set color temperature should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, child2_ep, 556) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + }, + } +) + +test.register_message_test( + "X and Y color values should report hue and saturation once both have been received", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Set color command should send the appropriate commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_children[child2_ep].id, + { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 72 } } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, child2_ep) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, child2_ep, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, child2_ep, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorControl.saturation(72)) + } + } +) + +test.register_message_test( + "Min and max level attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child1_ep, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child1_ep, 254) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child1_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 1, maximum = 100})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MinLevel:build_test_report_data(mock_device, child2_ep, 127) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.LevelControl.attributes.MaxLevel:build_test_report_data(mock_device, child2_ep, 203) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.switchLevel.levelRange({minimum = 50, maximum = 80})) + } + } +) + +test.register_message_test( + "Min and max color temp attributes set capability constraint for child devices", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_report_data(mock_device, child2_ep, 153) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_report_data(mock_device, child2_ep, 555) + } + }, + { + channel = "capability", + direction = "send", + message = mock_children[child2_ep]:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 1800, maximum = 6500})) + } + } ) test.register_coroutine_test( - "Child device profiles should be overriden for specific devices", function() - end, - { test_init = test_init_child_profile_override } + "Test child devices are created in order of their endpoints", + function() + end, + { test_init = test_init_parent_child_endpoints_non_sequential } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file From 14aa95e5d4e5c0fa940e258ebf470634fc33687b Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:47:52 -0600 Subject: [PATCH 306/449] Matter Switch Subdriver: Add Bilresa Support (#2579) --- .../matter-switch/profiles/ikea-scroll.yml | 29 +++ .../SmartThings/matter-switch/src/init.lua | 1 + .../src/sub_drivers/ikea_scroll/init.lua | 50 ++++ .../scroll_utils/device_configuration.lua | 35 +++ .../ikea_scroll/scroll_utils/fields.lua | 21 ++ .../ikea_scroll/scroll_utils/utils.lua | 31 +++ .../matter-switch/src/switch_utils/fields.lua | 5 +- .../src/test/test_ikea_scroll.lua | 223 ++++++++++++++++++ 8 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml new file mode 100644 index 0000000000..4dc77d026d --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -0,0 +1,29 @@ +name: ikea-scroll +components: + - id: main + label: Group 1 + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: group2 + label: Group 2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: group3 + label: Group 3 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 51454b9675..bd0fe0df8c 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -300,6 +300,7 @@ local matter_driver_template = { require("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), require("sub_drivers.eve_energy"), + require("sub_drivers.ikea_scroll"), require("sub_drivers.third_reality_mk1") } } diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua new file mode 100644 index 0000000000..bfdde071e7 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -0,0 +1,50 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local switch_utils = require "switch_utils.utils" +local scroll_utils = require "sub_drivers.ikea_scroll.scroll_utils.utils" +local scroll_cfg = require "sub_drivers.ikea_scroll.scroll_utils.device_configuration" + +local IkeaScrollLifecycleHandlers = {} + +-- prevent main driver device_added handling from running +function IkeaScrollLifecycleHandlers.device_added(driver, device) +end + +function IkeaScrollLifecycleHandlers.device_init(driver, device) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) + device:extend_device("subscribe", scroll_utils.subscribe) + device:subscribe() +end + +function IkeaScrollLifecycleHandlers.do_configure(driver, device) + scroll_cfg.match_profile(driver, device) +end + +function IkeaScrollLifecycleHandlers.driver_switched(driver, device) + scroll_cfg.match_profile(driver, device) +end + +function IkeaScrollLifecycleHandlers.info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + scroll_cfg.configure_buttons(device) + device:subscribe() + end +end + + +-- DEVICE TEMPLATE -- + +local ikea_scroll_handler = { + NAME = "Ikea Scroll Handler", + lifecycle_handlers = { + added = IkeaScrollLifecycleHandlers.device_added, + doConfigure = IkeaScrollLifecycleHandlers.do_configure, + driverSwitched = IkeaScrollLifecycleHandlers.driver_switched, + infoChanged = IkeaScrollLifecycleHandlers.info_changed, + init = IkeaScrollLifecycleHandlers.device_init, + }, + can_handle = scroll_utils.is_ikea_scroll +} + +return ikea_scroll_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua new file mode 100644 index 0000000000..cd2cee49ce --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua @@ -0,0 +1,35 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" +local switch_fields = require "switch_utils.fields" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollConfiguration = {} + +function IkeaScrollConfiguration.build_button_component_map(device) + local component_map = { + main = scroll_fields.ENDPOINTS_PRESS[1], + group2 = scroll_fields.ENDPOINTS_PRESS[2], + group3 = scroll_fields.ENDPOINTS_PRESS[3], + } + device:set_field(switch_fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) +end + +function IkeaScrollConfiguration.configure_buttons(device) + for _, ep in ipairs(scroll_fields.ENDPOINTS_PRESS) do + device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) + switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) + device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) + end +end + +function IkeaScrollConfiguration.match_profile(driver, device) + device:try_update_metadata({profile = "ikea-scroll"}) + IkeaScrollConfiguration.build_button_component_map(device) + IkeaScrollConfiguration.configure_buttons(device) +end + +return IkeaScrollConfiguration 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 new file mode 100644 index 0000000000..3e676b2912 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua @@ -0,0 +1,21 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" + +local IkeaScrollFields = {} + +-- PowerSource supported on Root Node +IkeaScrollFields.ENDPOINT_POWER_SOURCE = 0 + +-- Switch Endpoints used for basic press functionality +IkeaScrollFields.ENDPOINTS_PRESS = {3, 6, 9} + +-- Required Events for the ENDPOINTS_PRESS. Remove the default subscription to +-- InitialPress since this is a MultiPress device and InitialPress will be ignored. +IkeaScrollFields.switch_press_subscribed_events = { + clusters.Switch.events.MultiPressComplete.ID, + clusters.Switch.events.LongPress.ID, +} + +return IkeaScrollFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua new file mode 100644 index 0000000000..9be4d8601d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua @@ -0,0 +1,31 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local im = require "st.matter.interaction_model" +local clusters = require "st.matter.clusters" +local switch_utils = require "switch_utils.utils" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollUtils = {} + +function IkeaScrollUtils.is_ikea_scroll(opts, driver, device) + return switch_utils.get_product_override_field(device, "is_ikea_scroll") +end + +-- override subscribe function to prevent subscribing to additional events from the main driver +function IkeaScrollUtils.subscribe(device) + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + for _, ep_press in ipairs(scroll_fields.ENDPOINTS_PRESS) do + for _, switch_event in ipairs(scroll_fields.switch_press_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_press, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + local ib = im.InteractionInfoBlock( + scroll_fields.ENDPOINT_POWER_SOURCE, clusters.PowerSource.ID, clusters.PowerSource.attributes.BatPercentRemaining.ID + ) + subscribe_request:with_info_block(ib) + device:send(subscribe_request) +end + +return IkeaScrollUtils \ No newline at end of file diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 56ac1e5366..ddd554069a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -112,7 +112,10 @@ SwitchFields.vendor_overrides = { [0x1006] = { ignore_combo_switch_button = true }, -- 3 Buttons(Generic Switch), 1 Channel (Dimmable Light) [0x100A] = { ignore_combo_switch_button = true }, -- 1 Buttons(Generic Switch), 1 Channel (Dimmable Light) [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile - } + }, + [0x117C] = { -- IKEA_MANUFACTURER_ID + [0x8000] = { is_ikea_scroll = true } + }, } SwitchFields.switch_category_vendor_overrides = { diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua new file mode 100644 index 0000000000..f56b1e7738 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -0,0 +1,223 @@ +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +local mock_ikea_scroll = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("ikea-scroll.yml"), + manufacturer_info = {vendor_id = 0x117C, product_id = 0x8000, product_name = "Ikea Scroll"}, + label = "Ikea Scroll", + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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 | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + 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.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER"}, + }, + device_types = {{device_type_id = 0x000F, device_type_revision = 1}} -- GENERIC SWITCH + }, + } +}) + +local ENDPOINTS_PRESS = { 3, 6, 9 } + +-- the ikea scroll subdriver has overriden subscribe behavior +local function ikea_scroll_subscribe() + local CLUSTER_SUBSCRIBE_LIST ={ + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_ikea_scroll, ENDPOINTS_PRESS[1]) + for _, ep_press in ipairs(ENDPOINTS_PRESS) do + for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST) do + subscribe_request:merge(event:subscribe(mock_ikea_scroll, ep_press)) + end + end + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_ikea_scroll, 0)) + return subscribe_request +end + +local function expect_configure_buttons() + local button_attr = capabilities.button.button + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 3)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 6)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 9)}) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", button_attr.pushed({state_change = false}))) +end + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_ikea_scroll) + local subscribe_request = ikea_scroll_subscribe() + + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "init" }) + test.socket.matter:__expect_send({mock_ikea_scroll.id, subscribe_request}) + + mock_ikea_scroll:expect_metadata_update({ profile = "ikea-scroll" }) + mock_ikea_scroll:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + expect_configure_buttons() + test.socket.device_lifecycle:__queue_receive({ mock_ikea_scroll.id, "doConfigure" }) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Ensure Ikea Scroll Button initialization works as expected", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 3, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 6, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_ikea_scroll, 9, 3 + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.button.supportedButtonValues({"pushed", "double", "held", "pushed_3x"}, {visibility = {displayed = false}})) + }, + } +) + +test.run_registered_tests() \ No newline at end of file From 419a3ec4b6147a96ec893aa657979c73c7456ba0 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:40:20 -0600 Subject: [PATCH 307/449] re-add switch vendor override check, add vendor override test (#2603) --- .../src/switch_utils/device_configuration.lua | 4 ++ .../test/test_matter_switch_device_types.lua | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+) 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 4786f7b7fb..750c9eb50c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -194,6 +194,10 @@ function DeviceConfiguration.match_profile(driver, device) updated_profile = "light-color-level-fan" elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then updated_profile = "light-level-motion" + elseif generic_profile("plug-binary") or generic_profile("plug-level") then + if switch_utils.check_switch_category_vendor_overrides(device) then + updated_profile = string.gsub(updated_profile, "plug", "switch") + end elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since -- these may lose fingerprinted Kelvin ranges when dynamically profiled. diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 272ea3dad1..9cee75d69d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -92,6 +92,35 @@ local mock_device_dimmer = test.mock_device.build_test_matter_device({ } }) +local mock_device_switch_vendor_override = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("switch-binary.yml"), + manufacturer_info = { + vendor_id = 0x109B, + product_id = 0x1001, + }, + 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}, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- OnOff PlugIn Unit + } + } + } +}) + + local mock_device_color_dimmer = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("matter-thing.yml"), manufacturer_info = { @@ -485,6 +514,18 @@ local function test_init_color_dimmer() mock_device_color_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end +local function test_init_switch_vendor_override() + test.mock_device.add_test_device(mock_device_switch_vendor_override) + local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_switch_vendor_override) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "added" }) + test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "init" }) + test.socket.matter:__expect_send({mock_device_switch_vendor_override.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_switch_vendor_override.id, "doConfigure" }) + mock_device_switch_vendor_override:expect_metadata_update({ profile = "switch-binary" }) + mock_device_switch_vendor_override:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local function test_init_mounted_on_off_control() test.mock_device.add_test_device(mock_device_mounted_on_off_control) local cluster_subscribe_list = { @@ -677,6 +718,13 @@ test.register_coroutine_test( { test_init = test_init_onoff_client } ) +test.register_coroutine_test( + "Test init for device with requiring the switch category as a vendor override", + function() + end, + { test_init = test_init_switch_vendor_override } +) + test.register_coroutine_test( "Test init for mounted onoff control parent cluster as server", function() From 4f1be7e48ce659a97071bcc236994f71b5871e50 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:04:43 -0600 Subject: [PATCH 308/449] Matter AQS: Break AQS files further apart, add supported air quality sensor value handling (#2587) --- .../attribute_handlers.lua | 77 ++++++ .../device_configuration.lua | 4 +- .../{ => air_quality_sensor_utils}/fields.lua | 6 +- .../legacy_device_configuration.lua | 36 +-- .../air_quality_sensor_utils/utils.lua | 106 +++++++++ .../sub_drivers/air_quality_sensor/init.lua | 201 ++++------------ .../test/test_matter_air_quality_sensor.lua | 100 ++++---- ...test_matter_air_quality_sensor_modular.lua | 219 +++++++++++------- 8 files changed, 407 insertions(+), 342 deletions(-) create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua rename drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/{ => air_quality_sensor_utils}/device_configuration.lua (97%) rename drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/{ => air_quality_sensor_utils}/fields.lua (100%) rename drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/{ => air_quality_sensor_utils}/legacy_device_configuration.lua (60%) create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.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 new file mode 100644 index 0000000000..39cd6a8406 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua @@ -0,0 +1,77 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local capabilities = require "st.capabilities" +local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" +local aqs_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + +local AirQualitySensorAttributeHandlers = {} + + +-- [[ GENERIC CONCENTRATION MEASUREMENT CLUSTER ATTRIBUTES ]] + +function AirQualitySensorAttributeHandlers.measurement_unit_factory(capability_name) + return function(driver, device, ib, response) + device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) + end +end + +function AirQualitySensorAttributeHandlers.level_value_factory(attribute) + return function(driver, device, ib, response) + device:emit_event_for_endpoint(ib.endpoint_id, attribute(aqs_fields.level_strings[ib.data.value])) + end +end + +function AirQualitySensorAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit) + return function(driver, device, ib, response) + local reporting_unit = device:get_field(capability_name.."_unit") + + if reporting_unit == nil then + reporting_unit = aqs_fields.unit_default[capability_name] + device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) + end + + if reporting_unit then + local value = aqs_utils.unit_conversion(device, ib.data.value, reporting_unit, target_unit) + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = 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 = value, unit = aqs_fields.unit_strings[target_unit]})) + end + end + end +end + + +-- [[ AIR QUALITY CLUSTER ATTRIBUTES ]] -- + +function AirQualitySensorAttributeHandlers.air_quality_handler(driver, device, ib, response) + local state = ib.data.value + if state == 0 then -- Unknown + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) + elseif state == 1 then -- Good + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) + elseif state == 2 then -- Fair + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) + elseif state == 3 then -- Moderate + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) + elseif state == 4 then -- Poor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) + elseif state == 5 then -- VeryPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) + elseif state == 6 then -- ExtremelyPoor + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) + end +end + + +-- [[ PRESSURE MEASUREMENT CLUSTER ATTRIBUTES ]] -- + +function AirQualitySensorAttributeHandlers.pressure_measured_value_handler(driver, device, ib, response) + local pressure = st_utils.round(ib.data.value / 10.0) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) +end + +return AirQualitySensorAttributeHandlers diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua similarity index 97% rename from drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua rename to drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua index f1ea0506d0..a30eaed0f8 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/device_configuration.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua @@ -1,11 +1,11 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local version = require "version" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" -local fields = require "sub_drivers.air_quality_sensor.fields" -local version = require "version" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" local DeviceConfiguration = {} diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua similarity index 100% rename from drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua rename to drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua index 4cb435d8dd..1e4658c99e 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/fields.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua @@ -1,10 +1,10 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local utils = require "st.utils" local version = require "version" +local utils = require "st.utils" +local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" -- Include driver-side definitions when lua libs api version is < 10 if version.api < 10 then diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua similarity index 60% rename from drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua rename to drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua index aa76cb8d0e..bc45021341 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/legacy_device_configuration.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/legacy_device_configuration.lua @@ -1,42 +1,13 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" -local fields = require "sub_drivers.air_quality_sensor.fields" local sensor_utils = require "sensor_utils.utils" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" local LegacyDeviceConfiguration = {} -local function set_supported_health_concern_values(device, setter_function, cluster, cluster_ep) - -- read_datatype_value works since all the healthConcern capabilities' datatypes are equivalent to the one in airQualityHealthConcern - local read_datatype_value = capabilities.airQualityHealthConcern.airQualityHealthConcern - local supported_values = {read_datatype_value.unknown.NAME, read_datatype_value.good.NAME, read_datatype_value.unhealthy.NAME} - if cluster == clusters.AirQuality then - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.FAIR }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MODERATE }) > 0 then - table.insert(supported_values, 4, read_datatype_value.slightlyUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.VERY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.veryUnhealthy.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.EXTREMELY_POOR }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - else -- ConcentrationMeasurement clusters - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then - table.insert(supported_values, 3, read_datatype_value.moderate.NAME) - end - if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then - table.insert(supported_values, read_datatype_value.hazardous.NAME) - end - end - device:emit_event_for_endpoint(cluster_ep, setter_function(supported_values, { visibility = { displayed = false }})) -end - function LegacyDeviceConfiguration.create_level_measurement_profile(device) local meas_name, level_name = "", "" for _, cap in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do @@ -47,7 +18,6 @@ function LegacyDeviceConfiguration.create_level_measurement_profile(device) local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.LEVEL_INDICATION }) if #attr_eps > 0 then level_name = level_name .. fields.CONCENTRATION_MEASUREMENT_MAP[cap][1] - set_supported_health_concern_values(device, fields.CONCENTRATION_MEASUREMENT_MAP[cap][3], cluster, attr_eps[1]) end elseif (cap_id:match("Measurement$") or cap_id:match("Sensor$")) then local attr_eps = embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.NUMERIC_MEASUREMENT }) @@ -65,8 +35,6 @@ function LegacyDeviceConfiguration.match_profile(device) local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) local profile_name = "aqs" - local aq_eps = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) - set_supported_health_concern_values(device, capabilities.airQualityHealthConcern.supportedAirQualityValues, clusters.AirQuality, aq_eps[1]) if #temp_eps > 0 then profile_name = profile_name .. "-temp" 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 new file mode 100644 index 0000000000..e66d2da89d --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua @@ -0,0 +1,106 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" +local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + + +local AirQualitySensorUtils = {} + +function AirQualitySensorUtils.is_matter_air_quality_sensor(opts, driver, device) + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then + return true + end + end + end + + return false + end + +function AirQualitySensorUtils.supports_capability_by_id_modular(device, capability, component) + if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then + device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") + return false + end + for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do + local comp_id = component_capabilities[1] + local capability_ids = component_capabilities[2] + if (component == nil) or (component == comp_id) then + for _, cap in ipairs(capability_ids) do + if cap == capability then + return true + end + end + end + end + return false +end + +function AirQualitySensorUtils.unit_conversion(device, value, from_unit, to_unit) + local conversion_function = fields.conversion_tables[from_unit][to_unit] + if conversion_function == nil then + device.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 1 + end + + if value == nil then + device.log.info_with( {hub_logs = true} , "unit conversion value is nil") + return 1 + end + return conversion_function(value) +end + +local function get_supported_health_concern_values_for_air_quality(device) + local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern + local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME} + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.FAIR }) > 0 then + table.insert(supported_values, health_concern_datatype.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.MODERATE }) > 0 then + table.insert(supported_values, health_concern_datatype.slightlyUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.VERY_POOR }) > 0 then + table.insert(supported_values, health_concern_datatype.veryUnhealthy.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID, { feature_bitmap = clusters.AirQuality.types.Feature.EXTREMELY_POOR }) > 0 then + table.insert(supported_values, health_concern_datatype.hazardous.NAME) + end + return supported_values +end + +local function get_supported_health_concern_values_for_concentration_cluster(device, cluster) + -- note: health_concern_datatype is generic since all the healthConcern capabilities' datatypes are equivalent to those in airQualityHealthConcern + local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern + local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME} + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.MEDIUM_LEVEL }) > 0 then + table.insert(supported_values, health_concern_datatype.moderate.NAME) + end + if #embedded_cluster_utils.get_endpoints(device, cluster.ID, { feature_bitmap = cluster.types.Feature.CRITICAL_LEVEL }) > 0 then + table.insert(supported_values, health_concern_datatype.hazardous.NAME) + end + return supported_values +end + +function AirQualitySensorUtils.set_supported_health_concern_values(device) + -- handle AQ Health Concern, since this is a mandatory capability + local supported_aqs_values = get_supported_health_concern_values_for_air_quality(device) + local aqs_ep_ids = embedded_cluster_utils.get_endpoints(device, clusters.AirQuality.ID) or {} + device:emit_event_for_endpoint(aqs_ep_ids[1], capabilities.airQualityHealthConcern.supportedAirQualityValues(supported_aqs_values, { visibility = { displayed = false }})) + + for _, capability in ipairs(fields.CONCENTRATION_MEASUREMENT_PROFILE_ORDERING) do + -- all of these capabilities are optional, and capabilities stored in this field are for either a HealthConcern or a Measurement/Sensor + if device:supports_capability_by_id(capability.ID) and capability.ID:match("HealthConcern$") then + local cluster_info = fields.CONCENTRATION_MEASUREMENT_MAP[capability][2] + local supported_values_setter = fields.CONCENTRATION_MEASUREMENT_MAP[capability][3] + local supported_values = get_supported_health_concern_values_for_concentration_cluster(device, cluster_info) + local cluster_ep_ids = embedded_cluster_utils.get_endpoints(device, cluster_info.ID, { feature_bitmap = cluster_info.types.Feature.LEVEL_INDICATION }) or {} -- cluster associated with the supported capability + device:emit_event_for_endpoint(cluster_ep_ids[1], supported_values_setter(supported_values, { visibility = { displayed = false }})) + end + end +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 fea5e99e48..869f2d337b 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 @@ -1,13 +1,12 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local version = require "version" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local utils = require "st.utils" -local version = require "version" -local log = require "log" - -local fields = require "sub_drivers.air_quality_sensor.fields" +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" -- Include driver-side definitions when lua libs api version is < 10 if version.api < 10 then @@ -24,43 +23,6 @@ if version.api < 10 then clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement = require "embedded_clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement" end - --- SUBDRIVER UTILS -- - -local air_quality_sensor_utils = {} - -function air_quality_sensor_utils.is_matter_air_quality_sensor(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then - return true - end - end - end - - return false - end - -function air_quality_sensor_utils.supports_capability_by_id_modular(device, capability, component) - if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then - device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") - return false - end - for _, component_capabilities in ipairs(device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES)) do - local comp_id = component_capabilities[1] - local capability_ids = component_capabilities[2] - if (component == nil) or (component == comp_id) then - for _, cap in ipairs(capability_ids) do - if cap == capability then - return true - end - end - end - end - return false -end - - -- AIR QUALITY SENSOR LIFECYCLE HANDLERS -- local AirQualitySensorLifecycleHandlers = {} @@ -71,10 +33,10 @@ function AirQualitySensorLifecycleHandlers.do_configure(driver, device) device:send(cluster.attributes.MeasurementUnit:read(device)) end if version.api >= 14 and version.rpc >= 8 then - local modular_device_cfg = require "sub_drivers.air_quality_sensor.device_configuration" + local modular_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.device_configuration" modular_device_cfg.match_profile(device) else - local legacy_device_cfg = require "sub_drivers.air_quality_sensor.legacy_device_configuration" + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" legacy_device_cfg.match_profile(device) end end @@ -85,10 +47,10 @@ function AirQualitySensorLifecycleHandlers.driver_switched(driver, device) device:send(cluster.attributes.MeasurementUnit:read(device)) end if version.api >= 14 and version.rpc >= 8 then - local modular_device_cfg = require "sub_drivers.air_quality_sensor.device_configuration" + local modular_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.device_configuration" modular_device_cfg.match_profile(device) else - local legacy_device_cfg = require "sub_drivers.air_quality_sensor.legacy_device_configuration" + local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" legacy_device_cfg.match_profile(device) end end @@ -97,8 +59,9 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) and (version.api < 15 or version.rpc < 9) then -- assume that device is using a modular profile on 0.57 FW, override supports_capability_by_id -- library function to utilize optional capabilities - device:extend_device("supports_capability_by_id", air_quality_sensor_utils.supports_capability_by_id_modular) + device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular) end + aqs_utils.set_supported_health_concern_values(device) device:subscribe() end @@ -106,87 +69,13 @@ function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, a if device.profile.id ~= args.old_st_store.profile.id 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", air_quality_sensor_utils.supports_capability_by_id_modular) + device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular) end + aqs_utils.set_supported_health_concern_values(device) device:subscribe() end end --- ATTRIBUTE HANDLERS -- - -local sub_driver_handlers = {} - -function sub_driver_handlers.measurement_unit_factory(capability_name) - return function(driver, device, ib, response) - device:set_field(capability_name.."_unit", ib.data.value, {persist = true}) - end -end - -local function unit_conversion(value, from_unit, to_unit) - local conversion_function = fields.conversion_tables[from_unit][to_unit] - if conversion_function == nil 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 1 - end - - if value == nil then - log.info_with( {hub_logs = true} , "unit conversion value is nil") - return 1 - end - return conversion_function(value) -end - -function sub_driver_handlers.measured_value_factory(capability_name, attribute, target_unit) - return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if reporting_unit == nil then - reporting_unit = fields.unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - if reporting_unit then - local value = unit_conversion(ib.data.value, reporting_unit, target_unit) - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = 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]})) - end - end - end -end - -function sub_driver_handlers.level_value_factory(attribute) - return function(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(fields.level_strings[ib.data.value])) - end -end - -function sub_driver_handlers.air_quality_handler(driver, device, ib, response) - local state = ib.data.value - if state == 0 then -- Unknown - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unknown()) - elseif state == 1 then -- Good - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.good()) - elseif state == 2 then -- Fair - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.moderate()) - elseif state == 3 then -- Moderate - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.slightlyUnhealthy()) - elseif state == 4 then -- Poor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.unhealthy()) - elseif state == 5 then -- VeryPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.veryUnhealthy()) - elseif state == 6 then -- ExtremelyPoor - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.airQualityHealthConcern.airQualityHealthConcern.hazardous()) - end -end - -function sub_driver_handlers.pressure_measured_value_handler(driver, device, ib, response) - local pressure = utils.round(ib.data.value / 10.0) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.atmosphericPressureMeasurement.atmosphericPressure(pressure)) -end - -- SUBDRIVER TEMPLATE -- @@ -201,64 +90,64 @@ local matter_air_quality_sensor_handler = { matter_handlers = { attr = { [clusters.AirQuality.ID] = { - [clusters.AirQuality.attributes.AirQuality.ID] = sub_driver_handlers.air_quality_handler, + [clusters.AirQuality.attributes.AirQuality.ID] = attribute_handlers.air_quality_handler, }, [clusters.CarbonDioxideConcentrationMeasurement.ID] = { - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), - [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.carbonDioxideMeasurement.NAME, capabilities.carbonDioxideMeasurement.carbonDioxide, fields.units.PPM), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.carbonDioxideMeasurement.NAME), + [clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern), }, [clusters.CarbonMonoxideConcentrationMeasurement.ID] = { - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), - [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.carbonMonoxideMeasurement.NAME, capabilities.carbonMonoxideMeasurement.carbonMonoxideLevel, fields.units.PPM), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.carbonMonoxideMeasurement.NAME), + [clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.carbonMonoxideHealthConcern.carbonMonoxideHealthConcern), }, [clusters.FormaldehydeConcentrationMeasurement.ID] = { - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), - [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), - [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.formaldehydeMeasurement.NAME, capabilities.formaldehydeMeasurement.formaldehydeLevel, fields.units.PPM), + [clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.formaldehydeMeasurement.NAME), + [clusters.FormaldehydeConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.formaldehydeHealthConcern.formaldehydeHealthConcern), }, [clusters.NitrogenDioxideConcentrationMeasurement.ID] = { - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), - [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.nitrogenDioxideMeasurement.NAME, capabilities.nitrogenDioxideMeasurement.nitrogenDioxide, fields.units.PPM), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.nitrogenDioxideMeasurement.NAME), + [clusters.NitrogenDioxideConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.nitrogenDioxideHealthConcern.nitrogenDioxideHealthConcern) }, [clusters.OzoneConcentrationMeasurement.ID] = { - [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), - [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.ozoneMeasurement.NAME), - [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern) + [clusters.OzoneConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.ozoneMeasurement.NAME, capabilities.ozoneMeasurement.ozone, fields.units.PPM), + [clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.ozoneMeasurement.NAME), + [clusters.OzoneConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.ozoneHealthConcern.ozoneHealthConcern) }, [clusters.Pm1ConcentrationMeasurement.ID] = { - [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), - [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.veryFineDustSensor.NAME), - [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.veryFineDustSensor.NAME, capabilities.veryFineDustSensor.veryFineDustLevel, fields.units.UGM3), + [clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.veryFineDustSensor.NAME), + [clusters.Pm1ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern), }, [clusters.Pm10ConcentrationMeasurement.ID] = { - [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), - [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.dustSensor.NAME), - [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.dustSensor.NAME, capabilities.dustSensor.dustLevel, fields.units.UGM3), + [clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.dustSensor.NAME), + [clusters.Pm10ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.dustHealthConcern.dustHealthConcern), }, [clusters.Pm25ConcentrationMeasurement.ID] = { - [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), - [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.fineDustSensor.NAME), - [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.fineDustSensor.NAME, capabilities.fineDustSensor.fineDustLevel, fields.units.UGM3), + [clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.fineDustSensor.NAME), + [clusters.Pm25ConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.fineDustHealthConcern.fineDustHealthConcern), }, [clusters.PressureMeasurement.ID] = { - [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.pressure_measured_value_handler + [clusters.PressureMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.pressure_measured_value_handler }, [clusters.RadonConcentrationMeasurement.ID] = { - [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), - [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.radonMeasurement.NAME), - [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.radonHealthConcern.radonHealthConcern) + [clusters.RadonConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.radonMeasurement.NAME, capabilities.radonMeasurement.radonLevel, fields.units.PCIL), + [clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.radonMeasurement.NAME), + [clusters.RadonConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.radonHealthConcern.radonHealthConcern) }, [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.ID] = { - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = sub_driver_handlers.measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = sub_driver_handlers.measurement_unit_factory(capabilities.tvocMeasurement.NAME), - [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = sub_driver_handlers.level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern) + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasuredValue.ID] = attribute_handlers.measured_value_factory(capabilities.tvocMeasurement.NAME, capabilities.tvocMeasurement.tvocLevel, fields.units.PPB), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit.ID] = attribute_handlers.measurement_unit_factory(capabilities.tvocMeasurement.NAME), + [clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue.ID] = attribute_handlers.level_value_factory(capabilities.tvocHealthConcern.tvocHealthConcern) } } }, - can_handle = air_quality_sensor_utils.is_matter_air_quality_sensor + can_handle = aqs_utils.is_matter_air_quality_sensor } return matter_air_quality_sensor_handler 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 fbf5babd5c..1988563bc8 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 @@ -140,7 +140,7 @@ local mock_device_level = test.mock_device.build_test_matter_device({ }) local mock_device_co = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -169,7 +169,7 @@ local mock_device_co = test.mock_device.build_test_matter_device({ }) local mock_device_co2 = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -229,7 +229,12 @@ local mock_device_tvoc = test.mock_device.build_test_matter_device({ }) -- create test_init functions -local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) +local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes, expected_supported_values_setters) + test.mock_device.add_test_device(generic_mock_device) + test.socket.capability:__expect_send(generic_mock_device:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy",}, {visibility={displayed=false}}))) + if expected_supported_values_setters ~= nil then + expected_supported_values_setters() + end local subscribe_request = nil for _, attributes in pairs(generic_subscribed_attributes) do for _, attribute in ipairs(attributes) do @@ -241,7 +246,6 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) end -- TODO add tests for configuration using modular profiles @@ -329,7 +333,19 @@ local function test_init() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, }, } - initialize_mock_device(mock_device, subscribed_attributes) + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + initialize_mock_device(mock_device, subscribed_attributes, expected_supported_values_setters) end test.set_test_init_function(test_init) @@ -406,7 +422,19 @@ local function test_init_level() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, } } - initialize_mock_device(mock_device_level, subscribed_attributes) + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + initialize_mock_device(mock_device_level, subscribed_attributes, expected_supported_values_setters) end local function test_init_tvoc() @@ -435,26 +463,12 @@ local function test_init_co_co2() [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality }, - [capabilities.carbonMonoxideMeasurement.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonMonoxideHealthConcern.ID] = { - clusters.CarbonMonoxideConcentrationMeasurement.attributes.LevelValue, - }, } local attr_co2 = { [capabilities.airQualityHealthConcern.ID] = { clusters.AirQuality.attributes.AirQuality - }, - [capabilities.carbonDioxideMeasurement.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasuredValue, - clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit, - }, - [capabilities.carbonDioxideHealthConcern.ID] = { - clusters.CarbonDioxideConcentrationMeasurement.attributes.LevelValue, - }, + } } initialize_mock_device(mock_device_co, attr_co) @@ -463,7 +477,7 @@ end -- run the profile configuration tests -local function test_aqs_device_type_do_configure(generic_mock_device, expected_profile, expected_supported_values_setters) +local function test_aqs_device_type_do_configure(generic_mock_device, expected_profile) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) @@ -475,10 +489,6 @@ local function test_aqs_device_type_do_configure(generic_mock_device, expected_p test.socket.matter:__expect_send({generic_mock_device.id, clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) - test.socket.capability:__expect_send(generic_mock_device:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "moderate", "slightlyUnhealthy", "unhealthy"}, {visibility={displayed=false}}))) - if expected_supported_values_setters ~= nil then - expected_supported_values_setters() - end generic_mock_device:expect_metadata_update({ profile = expected_profile }) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -486,19 +496,7 @@ end test.register_coroutine_test( "Configure should read units from device and profile change as needed", function() - local expected_supported_values_setters = function() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device, "aqs-temp-humidity-all-level-all-meas", expected_supported_values_setters) + test_aqs_device_type_do_configure(mock_device, "aqs-temp-humidity-all-level-all-meas") end ) @@ -513,19 +511,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should read units from device and profile change as needed", function() - local expected_supported_values_setters = function() - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - test.socket.capability:__expect_send(mock_device_level:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device_level, "aqs-temp-humidity-all-level", expected_supported_values_setters) + test_aqs_device_type_do_configure(mock_device_level, "aqs-temp-humidity-all-level") end, { test_init = test_init_level } ) @@ -533,14 +519,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Configure should not catch co2, only co in the first check", function() - local expected_supported_co_values = function() - test.socket.capability:__expect_send(mock_device_co:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - local expected_supported_co2_values = function() - test.socket.capability:__expect_send(mock_device_co2:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) - end - test_aqs_device_type_do_configure(mock_device_co, "aqs-temp-humidity-all-meas", expected_supported_co_values) - test_aqs_device_type_do_configure(mock_device_co2, "aqs-temp-humidity-co2-pm25-tvoc-meas", expected_supported_co2_values) + test_aqs_device_type_do_configure(mock_device_co, "aqs-temp-humidity-all-meas") + test_aqs_device_type_do_configure(mock_device_co2, "aqs-temp-humidity-co2-pm25-tvoc-meas") end, { test_init = test_init_co_co2 } ) 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 0f186ee655..5b6b43f2f5 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 @@ -4,15 +4,13 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -test.set_rpc_version(8) +test.disable_startup_messages() -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-all-level-all-meas.yml"), +local mock_device_all = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -30,7 +28,7 @@ local mock_device = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER", feature_map = 3}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.CarbonMonoxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 3}, @@ -52,7 +50,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_common = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqs-temp-humidity-co2-pm25-tvoc-meas.yml"), + profile = t_utils.get_profile_definition("aqs.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -70,7 +68,7 @@ local mock_device_common = test.mock_device.build_test_matter_device({ { endpoint_id = 1, clusters = { - {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.AirQuality.ID, cluster_type = "SERVER", feature_map = 3}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.CarbonDioxideConcentrationMeasurement.ID, cluster_type = "SERVER", feature_map = 1}, @@ -84,25 +82,34 @@ local mock_device_common = test.mock_device.build_test_matter_device({ } }) --- create test_init functions -local function initialize_mock_device(generic_mock_device, generic_subscribed_attributes) - local subscribe_request = nil - for _, attributes in pairs(generic_subscribed_attributes) do - for _, attribute in ipairs(attributes) do - if subscribe_request == nil then - subscribe_request = attribute:subscribe(generic_mock_device) - else - subscribe_request:merge(attribute:subscribe(generic_mock_device)) - end - end - end - test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) - test.mock_device.add_test_device(generic_mock_device) - return subscribe_request +local function test_init_all() + test.mock_device.add_test_device(mock_device_all) + test.socket.device_lifecycle:__queue_receive({ mock_device_all.id, "init" }) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", + capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, + {visibility={displayed=false}})) + ) + -- on device create, a generic AQS device will be profiled as aqs, thus only subscribing to one attribute + local subscribe_request = clusters.AirQuality.attributes.AirQuality:subscribe(mock_device_all) + test.socket.matter:__expect_send({mock_device_all.id, subscribe_request}) +end + +local function test_init_common() + test.mock_device.add_test_device(mock_device_common) + test.socket.device_lifecycle:__queue_receive({ mock_device_common.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_common.id, "init" }) + test.socket.capability:__expect_send(mock_device_common:generate_test_message("main", + capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, + {visibility={displayed=false}})) + ) + -- on device create, a generic AQS device will be profiled as aqs, thus only subscribing to one attribute + local subscribe_request = clusters.AirQuality.attributes.AirQuality:subscribe(mock_device_common) + test.socket.matter:__expect_send({mock_device_common.id, subscribe_request}) end -local subscribe_request_all -local function test_init() +test.set_test_init_function(test_init_all) + +local function get_subscribe_request_all() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue @@ -184,11 +191,20 @@ local function test_init() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.LevelValue, }, } - subscribe_request_all = initialize_mock_device(mock_device, subscribed_attributes) + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in pairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device_all) + else + subscribe_request:merge(attribute:subscribe(mock_device_all)) + end + end + end + return subscribe_request end -local subscribe_request_common -local function test_init_common() +local function get_subscribe_request_common() local subscribed_attributes = { [capabilities.relativeHumidityMeasurement.ID] = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue @@ -214,12 +230,21 @@ local function test_init_common() clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit, }, } - subscribe_request_common = initialize_mock_device(mock_device_common, subscribed_attributes) + local subscribe_request = nil + for _, attributes in pairs(subscribed_attributes) do + for _, attribute in pairs(attributes) do + if subscribe_request == nil then + subscribe_request = attribute:subscribe(mock_device_common) + else + subscribe_request:merge(attribute:subscribe(mock_device_common)) + end + end + end + return subscribe_request end -test.set_test_init_function(test_init) -- run the profile configuration tests -local function test_aqs_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request) +local function test_aqs_device_type_update_modular_profile(generic_mock_device, expected_metadata, subscribe_request, expected_supported_values_setters) test.socket.device_lifecycle:__queue_receive({generic_mock_device.id, "doConfigure"}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) test.socket.matter:__expect_send({generic_mock_device.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) @@ -233,73 +258,93 @@ local function test_aqs_device_type_update_modular_profile(generic_mock_device, test.socket.matter:__expect_send({generic_mock_device.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) generic_mock_device:expect_metadata_update(expected_metadata) generic_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local device_info_copy = utils.deep_copy(generic_mock_device.raw_st_data) - device_info_copy.profile.id = "aqs-modular" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ generic_mock_device.id, "infoChanged", device_info_json }) + local updated_device_profile = t_utils.get_profile_definition("aqs-modular.yml", + {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.socket.device_lifecycle:__queue_receive(generic_mock_device:generate_info_changed({ profile = updated_device_profile })) + if expected_supported_values_setters ~= nil then + expected_supported_values_setters() + end test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end -local expected_metadata_all = { - optional_component_capabilities={ - { - "main", - { - "temperatureMeasurement", - "relativeHumidityMeasurement", - "carbonMonoxideMeasurement", - "carbonDioxideMeasurement", - "nitrogenDioxideMeasurement", - "ozoneMeasurement", - "formaldehydeMeasurement", - "veryFineDustSensor", - "fineDustSensor", - "dustSensor", - "radonMeasurement", - "tvocMeasurement", - "carbonMonoxideHealthConcern", - "carbonDioxideHealthConcern", - "nitrogenDioxideHealthConcern", - "ozoneHealthConcern", - "formaldehydeHealthConcern", - "veryFineDustHealthConcern", - "fineDustHealthConcern", - "dustHealthConcern", - "radonHealthConcern", - "tvocHealthConcern", - }, - }, - }, - profile="aqs-modular-temp-humidity", -} - test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - all clusters", function() - test_aqs_device_type_update_modular_profile(mock_device, expected_metadata_all, subscribe_request_all) - end -) - -local expected_metadata_common = { - optional_component_capabilities={ - { - "main", - { - "temperatureMeasurement", - "relativeHumidityMeasurement", - "carbonDioxideMeasurement", - "fineDustSensor", - "tvocMeasurement", + local expected_metadata_all = { + optional_component_capabilities={ + { + "main", + { + "temperatureMeasurement", + "relativeHumidityMeasurement", + "carbonMonoxideMeasurement", + "carbonDioxideMeasurement", + "nitrogenDioxideMeasurement", + "ozoneMeasurement", + "formaldehydeMeasurement", + "veryFineDustSensor", + "fineDustSensor", + "dustSensor", + "radonMeasurement", + "tvocMeasurement", + "carbonMonoxideHealthConcern", + "carbonDioxideHealthConcern", + "nitrogenDioxideHealthConcern", + "ozoneHealthConcern", + "formaldehydeHealthConcern", + "veryFineDustHealthConcern", + "fineDustHealthConcern", + "dustHealthConcern", + "radonHealthConcern", + "tvocHealthConcern", + }, + }, }, - }, - }, - profile="aqs-modular-temp-humidity", -} + profile="aqs-modular-temp-humidity", + } + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.carbonMonoxideHealthConcern.supportedCarbonMonoxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.carbonDioxideHealthConcern.supportedCarbonDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.nitrogenDioxideHealthConcern.supportedNitrogenDioxideValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.ozoneHealthConcern.supportedOzoneValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.formaldehydeHealthConcern.supportedFormaldehydeValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.veryFineDustHealthConcern.supportedVeryFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.fineDustHealthConcern.supportedFineDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.dustHealthConcern.supportedDustValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.radonHealthConcern.supportedRadonValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + test.socket.capability:__expect_send(mock_device_all:generate_test_message("main", capabilities.tvocHealthConcern.supportedTvocValues({"unknown", "good", "unhealthy"}, {visibility={displayed=false}}))) + end + local subscribe_request_all = get_subscribe_request_all() + test_aqs_device_type_update_modular_profile(mock_device_all, expected_metadata_all, subscribe_request_all, expected_supported_values_setters) + end, + { test_init = test_init_all } +) test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - common clusters", function() - test_aqs_device_type_update_modular_profile(mock_device_common, expected_metadata_common, subscribe_request_common) + local expected_metadata_common = { + optional_component_capabilities={ + { + "main", + { + "temperatureMeasurement", + "relativeHumidityMeasurement", + "carbonDioxideMeasurement", + "fineDustSensor", + "tvocMeasurement", + }, + }, + }, + profile="aqs-modular-temp-humidity", + } + local expected_supported_values_setters = function() + test.socket.capability:__expect_send(mock_device_common:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) + end + local subscribe_request_common = get_subscribe_request_common() + test_aqs_device_type_update_modular_profile(mock_device_common, expected_metadata_common, subscribe_request_common, expected_supported_values_setters) end, { test_init = test_init_common } ) From 9b2d2bf6f672b137081a54e1e82c0a1fc13ce368 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:30:00 -0600 Subject: [PATCH 309/449] Matter Sensor: Improve error handling for unit conversion (#2602) --- .../attribute_handlers.lua | 27 +++++++++---------- .../air_quality_sensor_utils/utils.lua | 14 ---------- 2 files changed, 12 insertions(+), 29 deletions(-) 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 39cd6a8406..345f7c5782 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 @@ -3,7 +3,6 @@ local st_utils = require "st.utils" local capabilities = require "st.capabilities" -local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" local aqs_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" local AirQualitySensorAttributeHandlers = {} @@ -25,20 +24,18 @@ end function AirQualitySensorAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit) return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") - - if reporting_unit == nil then - reporting_unit = aqs_fields.unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - if reporting_unit then - local value = aqs_utils.unit_conversion(device, ib.data.value, reporting_unit, target_unit) - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = 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 = value, unit = aqs_fields.unit_strings[target_unit]})) + 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 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 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 e66d2da89d..2462ced55d 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 @@ -40,20 +40,6 @@ function AirQualitySensorUtils.supports_capability_by_id_modular(device, capabil return false end -function AirQualitySensorUtils.unit_conversion(device, value, from_unit, to_unit) - local conversion_function = fields.conversion_tables[from_unit][to_unit] - if conversion_function == nil then - device.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 1 - end - - if value == nil then - device.log.info_with( {hub_logs = true} , "unit conversion value is nil") - return 1 - end - return conversion_function(value) -end - local function get_supported_health_concern_values_for_air_quality(device) local health_concern_datatype = capabilities.airQualityHealthConcern.airQualityHealthConcern local supported_values = {health_concern_datatype.unknown.NAME, health_concern_datatype.good.NAME, health_concern_datatype.unhealthy.NAME} From 63158a563e4c3a00add4c5d87e3db1bd5ee9d864 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:02:01 -0600 Subject: [PATCH 310/449] Matter Switch: Lazy load subdrivers if possible (#2525) Use the new `lazy_load_sub_driver_v2` api to lazy load subdrivers --- drivers/SmartThings/matter-switch/src/init.lua | 8 ++++---- .../src/sub_drivers/aqara_cube/can_handle.lua | 14 ++++++++++++++ .../src/sub_drivers/aqara_cube/init.lua | 12 +----------- .../src/sub_drivers/eve_energy/can_handle.lua | 18 ++++++++++++++++++ .../src/sub_drivers/eve_energy/init.lua | 17 +---------------- .../src/sub_drivers/ikea_scroll/can_handle.lua | 11 +++++++++++ .../src/sub_drivers/ikea_scroll/init.lua | 2 +- .../ikea_scroll/scroll_utils/utils.lua | 5 ----- .../third_reality_mk1/can_handle.lua | 14 ++++++++++++++ .../src/sub_drivers/third_reality_mk1/init.lua | 16 +--------------- .../matter-switch/src/switch_utils/utils.lua | 10 ++++++++++ 11 files changed, 75 insertions(+), 52 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bd0fe0df8c..3c6c6a302a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -297,11 +297,11 @@ local matter_driver_template = { }, supported_capabilities = fields.supported_capabilities, sub_drivers = { - require("sub_drivers.aqara_cube"), + switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), - require("sub_drivers.eve_energy"), - require("sub_drivers.ikea_scroll"), - require("sub_drivers.third_reality_mk1") + 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_mk1") } } diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua new file mode 100644 index 0000000000..dcecbc86ee --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" + +return function(opts, driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + local name = string.format("%s", device.manufacturer_info.product_name) + if string.find(name, "Aqara Cube T1 Pro") then + return true, require("sub_drivers.aqara_cube") + end + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua index b6ee56c261..4e6b729fa3 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua @@ -21,16 +21,6 @@ local INITIAL_PRESS_ONLY = "__initial_press_only" -- for devices that support MS local CUBEACTION_TIMER = "__cubeAction_timer" local CUBEACTION_TIME = 3 -local function is_aqara_cube(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - local name = string.format("%s", device.manufacturer_info.product_name) - if string.find(name, "Aqara Cube T1 Pro") then - return true - end - end - return false -end - local callback_timer = function(device) return function() device:emit_event(cubeAction.cubeAction("noAction")) @@ -240,7 +230,7 @@ local aqara_cube_handler = { } }, }, - can_handle = is_aqara_cube + can_handle = require("sub_drivers.aqara_cube.can_handle") } return aqara_cube_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua new file mode 100644 index 0000000000..369b93b8de --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" +local fields = require "switch_utils.fields" +local switch_utils = require "switch_utils.utils" + +return function(opts, driver, device) + local EVE_MANUFACTURER_ID = 0x130A + -- this sub driver does NOT support child devices, and ONLY supports Eve devices + -- that do NOT support the Electrical Sensor device type + if device.network_type == device_lib.NETWORK_TYPE_MATTER and + device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and + #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) == 0 then + return true, require("sub_drivers.eve_energy") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 1df33b19b0..8222fc1378 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -11,14 +11,11 @@ local cluster_base = require "st.matter.cluster_base" local st_utils = require "st.utils" local data_types = require "st.matter.data_types" local device_lib = require "st.device" -local switch_utils = require "switch_utils.utils" -local fields = require "switch_utils.fields" local SWITCH_INITIALIZED = "__switch_intialized" local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" local ON_OFF_STATES = "ON_OFF_STATES" -local EVE_MANUFACTURER_ID = 0x130A local PRIVATE_CLUSTER_ID = 0x130AFC01 local PRIVATE_ATTR_ID_WATT = 0x130A000A @@ -38,18 +35,6 @@ local MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in s -- Eve specifics ------------------------------------------------------------------------------------- -local function is_eve_energy_products(opts, driver, device) - -- this sub driver does NOT support child devices, and ONLY supports Eve devices - -- that do NOT support the Electrical Sensor device type - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == EVE_MANUFACTURER_ID and - #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR) == 0 then - return true - end - - return false -end - -- Return a ISO 8061 formatted timestamp in UTC (Z) -- @return e.g. 2022-02-02T08:00:00Z local function epoch_to_iso8601(time) @@ -370,7 +355,7 @@ local eve_energy_handler = { capabilities.energyMeter, capabilities.powerConsumptionReport }, - can_handle = is_eve_energy_products + can_handle = require("sub_drivers.eve_energy.can_handle") } return eve_energy_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua new file mode 100644 index 0000000000..1245fae5c9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local switch_utils = require "switch_utils.utils" + +return function(opts, driver, device) + if switch_utils.get_product_override_field(device, "is_ikea_scroll") then + return true, require("sub_drivers.ikea_scroll") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua index bfdde071e7..6e21a56ac0 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -44,7 +44,7 @@ local ikea_scroll_handler = { infoChanged = IkeaScrollLifecycleHandlers.info_changed, init = IkeaScrollLifecycleHandlers.device_init, }, - can_handle = scroll_utils.is_ikea_scroll + can_handle = require("sub_drivers.ikea_scroll.can_handle") } return ikea_scroll_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua index 9be4d8601d..67ba2acba5 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua @@ -3,15 +3,10 @@ local im = require "st.matter.interaction_model" local clusters = require "st.matter.clusters" -local switch_utils = require "switch_utils.utils" local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" local IkeaScrollUtils = {} -function IkeaScrollUtils.is_ikea_scroll(opts, driver, device) - return switch_utils.get_product_override_field(device, "is_ikea_scroll") -end - -- override subscribe function to prevent subscribing to additional events from the main driver function IkeaScrollUtils.subscribe(device) local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) 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 new file mode 100644 index 0000000000..f3f5342989 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local device_lib = require "st.device" + +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 + return true, require("sub_drivers.third_reality_mk1") + end + return false +end 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 f3c779906f..1572886089 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 @@ -3,9 +3,7 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local device_lib = require "st.device" local im = require "st.matter.interaction_model" -local log = require "log" local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" @@ -13,18 +11,6 @@ local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -- Third Reality MK1 specifics ------------------------------------------------------------------------------------- -local THIRD_REALITY_MK1_FINGERPRINT = { vendor_id = 0x1407, product_id = 0x1388 } - -local function is_third_reality_mk1(opts, driver, device) - 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 - log.info("Using Third Reality MK1 sub driver") - return true - end - return false -end - local function endpoint_to_component(device, ep) local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} for component, endpoint in pairs(map) do @@ -121,7 +107,7 @@ local third_reality_mk1_handler = { supported_capabilities = { capabilities.button }, - can_handle = is_third_reality_mk1 + can_handle = require("sub_drivers.third_reality_mk1.can_handle") } return third_reality_mk1_handler diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 1e61f60c74..68d588f2dd 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -416,4 +416,14 @@ function utils.lazy_load(sub_driver_name) end end +function utils.lazy_load_if_possible(sub_driver_name) + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end + return utils From 4983848c9ae5dcb2dbc9fb1cad178386c5be87f6 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:34 -0600 Subject: [PATCH 311/449] CHAD-17062: zigbee-bed lazy loading subdrivers --- drivers/SmartThings/zigbee-bed/src/init.lua | 20 +++---------- .../zigbee-bed/src/lazy_load_subdriver.lua | 15 ++++++++++ .../src/shus-mattress/can_handle.lua | 14 +++++++++ .../src/shus-mattress/custom_capabilities.lua | 15 ++-------- .../src/shus-mattress/custom_clusters.lua | 15 ++-------- .../src/shus-mattress/fingerprints.lua | 8 +++++ .../zigbee-bed/src/shus-mattress/init.lua | 29 +++---------------- .../zigbee-bed/src/sub_drivers.lua | 8 +++++ .../src/test/test_shus_mattress.lua | 15 ++-------- 9 files changed, 59 insertions(+), 80 deletions(-) create mode 100644 drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-bed/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-bed/src/init.lua b/drivers/SmartThings/zigbee-bed/src/init.lua index d59cf4e012..9f464c38ce 100755 --- a/drivers/SmartThings/zigbee-bed/src/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -20,9 +10,7 @@ local zigbee_bed_template = { supported_capabilities = { capabilities.refresh, }, - sub_drivers = { - require("shus-mattress"), - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua new file mode 100644 index 0000000000..0adcc5e46e --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_shus_products(opts, driver, device) + local FINGERPRINTS = require("shus-mattress.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("shus-mattress") + end + end + return false +end + +return is_shus_products diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua index 2c3dab5292..fdd5ab0004 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_capabilities.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua index 35baddbaa9..db220da460 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/custom_clusters.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua new file mode 100644 index 0000000000..2d6c80ee1a --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "SHUS", model = "SX-1" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua index 08109f35ce..65cf64e446 100755 --- a/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua +++ b/drivers/SmartThings/zigbee-bed/src/shus-mattress/init.lua @@ -1,25 +1,12 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local custom_clusters = require "shus-mattress/custom_clusters" local custom_capabilities = require "shus-mattress/custom_capabilities" -local FINGERPRINTS = { - { mfr = "SHUS", model = "SX-1" } -} -- ############################# -- # Attribute handlers define # @@ -155,14 +142,6 @@ end local function do_configure(driver, device) end -local function is_shus_products(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 -- ################# -- # Handlers bind # @@ -229,7 +208,7 @@ local shus_smart_mattress = { ["stateControl"] = process_capabilities_factory("stateControl","yoga") } }, - can_handle = is_shus_products + can_handle = require("shus-mattress.can_handle"), } return shus_smart_mattress diff --git a/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua b/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua new file mode 100644 index 0000000000..3e78841f3b --- /dev/null +++ b/drivers/SmartThings/zigbee-bed/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("shus-mattress"), +} +return sub_drivers 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 d3922a3bae..7e5ade0cf5 100755 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From 356c1d0fd17d42bc1f09372305efffc821f8b61a Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 5 Dec 2025 11:58:51 -0600 Subject: [PATCH 312/449] add ResetEnergyMeter command --- .../SmartThings/matter-switch/src/init.lua | 3 + .../switch_handlers/attribute_handlers.lua | 5 + .../switch_handlers/capability_handlers.lua | 23 ++- .../matter-switch/src/switch_utils/fields.lua | 1 + .../matter-switch/src/switch_utils/utils.lua | 3 +- .../src/test/test_electrical_sensor_set.lua | 193 +++++++++++++++++- 6 files changed, 225 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bd0fe0df8c..ecb75e2a82 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -274,6 +274,9 @@ local matter_driver_template = { [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = capability_handlers.handle_set_color_temperature, }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = capability_handlers.handle_reset_energy_meter, + }, [capabilities.fanMode.ID] = { [capabilities.fanMode.commands.setFanMode.NAME] = capability_handlers.handle_set_fan_mode }, 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 40b1f6a44c..756a402de6 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -284,6 +284,11 @@ function AttributeHandlers.energy_imported_factory(is_periodic_report) device, ib, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME ) or 0 energy_imported_wh = energy_imported_wh + energy_meter_latest_state + else + -- the field containing the offset may be associated with a child device + local field_device = switch_utils.find_child(device, ib.endpoint_id) or device + local energy_meter_offset = field_device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + energy_imported_wh = energy_imported_wh - energy_meter_offset end device:emit_event_for_endpoint(ib.endpoint_id, capabilities.energyMeter.energy({ value = energy_imported_wh, unit = "Wh" })) switch_utils.report_power_consumption_to_st_energy(device, ib.endpoint_id, energy_imported_wh) diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index 77927ef1d2..2c5241de3a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -168,4 +168,25 @@ function CapabilityHandlers.handle_fan_speed_set_percent(driver, device, cmd) device:send(clusters.FanControl.attributes.PercentSetting:write(device, fan_ep, speed)) end -return CapabilityHandlers \ No newline at end of file + +-- [[ ENERGY METER CAPABILITY COMMANDS ]] -- + +--- +--- If a Cumulative Reporting device, this will store the most recent energy meter reading, and all subsequent reports will have this value subtracted +--- from the value reported. Matter, like Zigbee and unlike Z-Wave, does not provide a way to reset the value to zero, so this is an attempt at a workaround. +--- In the case it is a Periodic Reporting device, the reports do not need to be offset, so setting the current energy to 0.0 will do the same thing. +--- +function CapabilityHandlers.handle_reset_energy_meter(driver, device, cmd) + local energy_meter_latest_state = device:get_latest_state(cmd.component, capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) or 0.0 + if energy_meter_latest_state ~= 0.0 then + device:emit_component_event(device.profile.components[cmd.component], capabilities.energyMeter.energy({value = 0.0, unit = "Wh"})) + -- note: field containing cumulative reports supported is only set on the parent device + local field_device = device:get_parent_device() or device + if field_device:get_field(fields.CUMULATIVE_REPORTS_SUPPORTED) then + local current_offset = device:get_field(fields.ENERGY_METER_OFFSET) or 0.0 + device:set_field(fields.ENERGY_METER_OFFSET, current_offset + energy_meter_latest_state, {persist=true}) + end + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index ddd554069a..f0fd0166b4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -155,6 +155,7 @@ SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", } +SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset" SwitchFields.CUMULATIVE_REPORTS_SUPPORTED = "__cumulative_reports_supported" SwitchFields.LAST_IMPORTED_REPORT_TIMESTAMP = "__last_imported_report_timestamp" SwitchFields.MINIMUM_ST_ENERGY_REPORT_INTERVAL = (15 * 60) -- 15 minutes, reported in seconds diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 1e61f60c74..e3c3f8a100 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -76,7 +76,8 @@ function utils.mired_to_kelvin(value, minOrMax) end function utils.get_product_override_field(device, override_key) - if fields.vendor_overrides[device.manufacturer_info.vendor_id] + if device.manufacturer_info + and fields.vendor_overrides[device.manufacturer_info.vendor_id] and fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id] then return fields.vendor_overrides[device.manufacturer_info.vendor_id][device.manufacturer_info.product_id][override_key] 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 3df62056ca..412c773540 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 @@ -161,8 +161,17 @@ local periodic_report_val_23 = { reactive_energy = 0 } +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("plug-level-energy-powerConsumption.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 4), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) +}) + local function test_init() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) + local subscribe_request = subscribed_attributes[1]:subscribe(mock_device) for i, cluster in ipairs(subscribed_attributes) do if i > 1 then @@ -431,7 +440,6 @@ test.register_coroutine_test( test.register_coroutine_test( "Test profile change on init for Electrical Sensor device type", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) @@ -463,6 +471,189 @@ test.register_coroutine_test( { test_init = test_init_periodic } ) +test.register_coroutine_test( + "Test resetEnergyMeter command on parent and child for CumulativeEnergyImported", + function() + + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + + -- this block needs to run to set the requisite fields. It is tested on its own elsewhere + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 2, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, 4, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 1, {uint32(2)})}) + test.socket.matter:__queue_receive({ mock_device.id, clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(mock_device, 3, {uint32(4)})}) + mock_device:expect_metadata_update({ profile = "plug-level-power-energy-powerConsumption" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "nil 2", + profile = "plug-level-energy-powerConsumption", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", 4) + }) + -- end of block + + -- Initial Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 19.0 + })) + ) + + -- Initial Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 19.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the parent). + + + test.wait_for_events() + test.mock_time.advance_time(1500) + + + -- Parent call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_device.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + -- Child call to resetEnergyMeter + test.socket.capability:__queue_receive({mock_child.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + -- Second Child Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 3, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + test.socket.capability:__expect_send( + mock_child:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:40:00Z", + deltaEnergy = 0.0, + energy = 20.0 + })) + ) + + -- Second Parent Energy Report + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.ElectricalEnergyMeasurement.server.attributes.CumulativeEnergyImported:build_test_report_data( + mock_device, 1, cumulative_report_val_39 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 20.0, unit = "Wh" })) + ) + -- no powerConsumptionReport will be emitted now, since it has not been 15 minutes since the previous report (even though it was the child). + end, + { test_init = test_init } +) + +test.register_coroutine_test( + "Test resetEnergyMeter command on device for PeriodicEnergyImported", + function() + test.mock_time.advance_time(901) -- move time 15 minutes past 0 (this can be assumed to be true in practice in all cases) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 23.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:00:00Z", + ["end"] = "1970-01-01T00:15:00Z", + deltaEnergy = 0.0, + energy = 23.0 + })) + ) + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, periodic_report_val_23 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 46.0, unit="Wh"})) + ) + + test.wait_for_events() + test.mock_time.advance_time(2000) + + test.socket.capability:__queue_receive({mock_device_periodic.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {}}}) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + ) + + test.wait_for_events() + + test.socket.matter:__queue_receive( + { + mock_device_periodic.id, + clusters.ElectricalEnergyMeasurement.server.attributes.PeriodicEnergyImported:build_test_report_data( + mock_device_periodic, 1, cumulative_report_val_19 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.energyMeter.energy({value = 19.0, unit="Wh"})) + ) + test.socket.capability:__expect_send( + mock_device_periodic:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({ + start = "1970-01-01T00:15:01Z", + ["end"] = "1970-01-01T00:48:20Z", + deltaEnergy = -4.0, + energy = 19.0 + })) + ) + end, + { test_init = test_init_periodic } +) + test.register_message_test( "Set level command should send the appropriate commands", { From 9bd3ef0e60b77929c8cc4bb0b79d5c3eba700ac1 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 5 Dec 2025 11:41:12 -0800 Subject: [PATCH 313/449] Osram SMART MAT A53 DIM FILGD 824 E27 --- 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 7703b38b87..a13168ac8b 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -950,6 +950,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0A9B deviceProfileName: light-level-colorTemperature + - id: 4489/2686 + deviceLabel: SMART MAT A53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7E + deviceProfileName: light-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From aa71a9a9cd3b17cf00699b3dec8d24db6052ad83 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:19 -0600 Subject: [PATCH 314/449] CHAD-17070: zigbee-lock lazy loading of subdrivers --- .../zigbee-lock/src/configurations.lua | 15 ++------ drivers/SmartThings/zigbee-lock/src/init.lua | 23 +++---------- .../zigbee-lock/src/lazy_load_subdriver.lua | 15 ++++++++ .../src/lock-without-codes/can_handle.lua | 14 ++++++++ .../src/lock-without-codes/fingerprints.lua | 9 +++++ .../src/lock-without-codes/init.lua | 30 +++------------- .../zigbee-lock/src/lock_utils.lua | 15 ++------ .../zigbee-lock/src/samsungsds/can_handle.lua | 11 ++++++ .../zigbee-lock/src/samsungsds/init.lua | 20 +++-------- .../zigbee-lock/src/sub_drivers.lua | 11 ++++++ .../zigbee-lock/src/test/test_c2o_lock.lua | 15 ++------ .../src/test/test_generic_lock_migration.lua | 17 ++-------- ..._yale_fingerprint_bad_battery_reporter.lua | 15 ++------ .../zigbee-lock/src/test/test_zigbee_lock.lua | 15 ++------ .../test/test_zigbee_lock_code_migration.lua | 15 ++------ .../test_zigbee_yale-bad-battery-reporter.lua | 15 ++------ .../test_zigbee_yale-fingerprint-lock.lua | 15 ++------ .../zigbee-lock/src/test/test_zigbee_yale.lua | 15 ++------ .../src/yale-fingerprint-lock/can_handle.lua | 14 ++++++++ .../yale-fingerprint-lock/fingerprints.lua | 11 ++++++ .../src/yale-fingerprint-lock/init.lua | 32 +++-------------- .../zigbee-lock/src/yale/can_handle.lua | 11 ++++++ .../SmartThings/zigbee-lock/src/yale/init.lua | 23 +++---------- .../zigbee-lock/src/yale/sub_drivers.lua | 8 +++++ .../yale-bad-battery-reporter/can_handle.lua | 14 ++++++++ .../fingerprints.lua | 13 +++++++ .../yale/yale-bad-battery-reporter/init.lua | 34 +++---------------- 27 files changed, 177 insertions(+), 268 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/configurations.lua b/drivers/SmartThings/zigbee-lock/src/configurations.lua index a2429252b0..88e4e59f80 100644 --- a/drivers/SmartThings/zigbee-lock/src/configurations.lua +++ b/drivers/SmartThings/zigbee-lock/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..94f5adc0c4 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" @@ -445,12 +435,7 @@ local zigbee_lock_driver = { [capabilities.refresh.commands.refresh.NAME] = refresh } }, - sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { doConfigure = do_configure, added = device_added, diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end 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 new file mode 100644 index 0000000000..543e43a8b1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua @@ -0,0 +1,14 @@ +-- 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-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua new file mode 100644 index 0000000000..63ae82b46c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return LOCK_WITHOUT_CODES_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index 7272991459..e5c6de3408 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local configurationMap = require "configurations" local clusters = require "st.zigbee.zcl.clusters" @@ -19,19 +9,7 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_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_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -95,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("lock-without-codes.can_handle"), } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..a02a59963c 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- 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" diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua new file mode 100644 index 0000000000..c483b2fe27 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samsungsds_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SAMSUNG SDS" then + return true, require("samsungsds") + end + return false +end + +return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index b529dd3fd1..fff290df5d 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -112,9 +102,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("samsungsds.can_handle"), } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..ff4bf8980d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/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("samsungsds"), + lazy_load_if_possible("yale"), + 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_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index b6fa3d1323..146c628b8b 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua index ed4ce6e3cc..f287300f60 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -45,4 +34,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua index d499d7ff66..4f50c3c24a 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 80d10d092e..3ed037cd54 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 index 1aa9432933..7950e3f62d 100644 --- 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 index ee1745e3b7..b8f4c386d9 100644 --- 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 2255c063a3..7cda71cdb3 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 34b6881028..75ad49a1f5 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 new file mode 100644 index 0000000000..a80632bf80 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local yale_fingerprint_lock_models = 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 + return true, require("yale-fingerprint-lock") + end + end + return false +end + +return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua new file mode 100644 index 0000000000..b3db27d719 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return YALE_FINGERPRINT_LOCK 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 9d0a0b4148..b78d043784 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -19,21 +9,7 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) @@ -48,7 +24,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("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/yale/can_handle.lua new file mode 100644 index 0000000000..54340c7811 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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") + end + return false +end + +return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 73e984036e..8ba98b2aa8 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" @@ -151,11 +142,7 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - - sub_drivers = { require("yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + sub_drivers = require("yale.sub_drivers"), } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua new file mode 100644 index 0000000000..4b546979d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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 new file mode 100644 index 0000000000..67169e9268 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,14 @@ +-- 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 diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua new file mode 100644 index 0000000000..cbb7c3404f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return BAD_YALE_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..3b77f32563 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua @@ -1,37 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) @@ -46,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("yale.yale-bad-battery-reporter.can_handle"), } return bad_yale_driver From 1224494b99a15132f177b51cd0ccbcd082da2499 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:25 -0600 Subject: [PATCH 315/449] CHAD-17071: zigbee-presence-sensor lazy loading of subdrivers --- .../src/aqara/can_handle.lua | 13 +++++++++++ .../zigbee-presence-sensor/src/aqara/init.lua | 11 ++++------ .../src/arrival-sensor-v1/can_handle.lua | 12 ++++++++++ .../src/arrival-sensor-v1/init.lua | 22 ++++--------------- .../zigbee-presence-sensor/src/init.lua | 21 ++++-------------- .../src/lazy_load_subdriver.lua | 15 +++++++++++++ .../src/presence_utils.lua | 15 ++----------- .../src/sub_drivers.lua | 9 ++++++++ .../src/test/test_st_arrival_sensor_v1.lua | 15 ++----------- .../src/test/test_zigbee_presence_sensor.lua | 15 ++----------- 10 files changed, 67 insertions(+), 81 deletions(-) create mode 100644 drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..33a0bde838 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device, ...) + local FINGERPRINTS = { mfr = "aqara", model = "lumi.motion.ac01" } + + if device:get_manufacturer() == FINGERPRINTS.mfr and device:get_model() == FINGERPRINTS.model then + return true, require("aqara") + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua index b9a0bfb001..1fd79e3875 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -19,12 +22,6 @@ local SENSITIVITY = "stse.sensitivity" local RESET_PRESENCE = "stse.resetPresence" local APP_DISTANCE = "stse.approachDistance" -local FINGERPRINTS = { mfr = "aqara", model = "lumi.motion.ac01" } - -local is_aqara_products = function(opts, driver, device, ...) - return device:get_manufacturer() == FINGERPRINTS.mfr and device:get_model() == FINGERPRINTS.model -end - local function device_init(driver, device) -- no action end @@ -104,7 +101,7 @@ local aqara_fp1_handler = { doConfigure = do_configure, infoChanged = device_info_changed }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_fp1_handler diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua new file mode 100644 index 0000000000..d848d46caa --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function arrival_sensor_v1_can_handle(opts, driver, device, ...) + -- excluding Aqara device and tagv4 + if device:get_manufacturer() ~= "aqara" and device:get_model() ~= "tagv4" then + return true, require("arrival-sensor-v1") + end + return false +end + +return arrival_sensor_v1_can_handle diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua index b7bece2efe..81d9461a38 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/arrival-sensor-v1/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Spec Utils local zcl_messages = require "st.zigbee.zcl" @@ -41,10 +31,6 @@ local presence_utils = require "presence_utils" local CHECKIN_INTERVAL = 20 -- seconds -local function arrival_sensor_v1_can_handle(opts, driver, device, ...) - -- excluding Aqara device and tagv4 - return device:get_manufacturer() ~= "aqara" and device:get_model() ~= "tagv4" -end local function legacy_battery_handler(self, device, zb_rx) local battery_value = string.byte(zb_rx.body.zcl_body.body_bytes) @@ -144,7 +130,7 @@ local arrival_sensor_v1 = { added = added_handler, init = init_handler }, - can_handle = arrival_sensor_v1_can_handle + can_handle = require("arrival-sensor-v1.can_handle"), } return arrival_sensor_v1 diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua index cc1611f2ec..261bc90922 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -204,10 +194,7 @@ local zigbee_presence_driver = { }, -- Custom handler for every Zigbee message zigbee_message_handler = all_zigbee_message_handler, - sub_drivers = { - require("aqara"), - require("arrival-sensor-v1") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua b/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua index a68de0f95e..968d75ea54 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/presence_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local presence_utils = {} diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..1340826663 --- /dev/null +++ b/drivers/SmartThings/zigbee-presence-sensor/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("aqara"), + lazy_load_if_possible("arrival-sensor-v1"), +} +return sub_drivers 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 4c2533036d..123edc9de1 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 55a0d7cf51..6957148647 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From cedba53c2e3bb4eee0f9c2aaf3ce6b2048d7bbc7 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 8 Dec 2025 11:28:16 -0600 Subject: [PATCH 316/449] deprecate old Matter Lock profiles --- .../SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml | 1 + drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml | 1 + drivers/SmartThings/matter-lock/profiles/base-lock.yml | 1 + .../matter-lock/profiles/lock-lockalarm-batteryLevel.yml | 1 + .../matter-lock/profiles/lock-lockalarm-nobattery.yml | 1 + drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml | 1 + .../matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml | 1 + .../SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml | 1 + .../matter-lock/profiles/lock-without-codes-batteryLevel.yml | 1 + .../matter-lock/profiles/lock-without-codes-nobattery.yml | 1 + drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml | 1 + .../matter-lock/profiles/nonfunctional-lock-batteryLevel.yml | 1 + drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml | 1 + 13 files changed, 13 insertions(+) diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml index 257a3c6e3f..093c741219 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml index 769e955219..c223facfbf 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/base-lock.yml b/drivers/SmartThings/matter-lock/profiles/base-lock.yml index 094aa523cc..7d69a5c8e4 100755 --- a/drivers/SmartThings/matter-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/matter-lock/profiles/base-lock.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: base-lock components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml index f946fd27cc..83b8c097f7 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml index 96641392bf..135a1e1986 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml index 007d01aa2f..ed0dd126e6 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-lockalarm.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-lockalarm components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml index f55fbe012c..abd55420fd 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-nocodes-notamper-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml index 2371c84732..20dec32602 100644 --- a/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-nocodes-notamper.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-nocodes-notamper components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml index 15ce853cfd..2d577cd19b 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml index 8de4bb1a5c..059b65e9f9 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes-nobattery.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes-nobattery components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml b/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml index 772b742f20..86bc5e45d3 100755 --- a/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml +++ b/drivers/SmartThings/matter-lock/profiles/lock-without-codes.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: lock-without-codes components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml index fc3371b31c..cf342d9548 100644 --- a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml +++ b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock-batteryLevel.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: nonfunctional-lock-batteryLevel components: - id: main diff --git a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml index 1990cbc025..e662780d55 100644 --- a/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml +++ b/drivers/SmartThings/matter-lock/profiles/nonfunctional-lock.yml @@ -1,3 +1,4 @@ +# Deprecated: do not use this profile for device fingerprinting. name: nonfunctional-lock components: - id: main From 121b3c05f956cc77615ac127376b3943f0dff273 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 8 Dec 2025 10:02:30 -0800 Subject: [PATCH 317/449] add missing copyright notice --- .../src/frient-sensor/air-quality/init.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua index e33c1b07e7..e9ca8e85cf 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua @@ -1,3 +1,6 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local util = require "st.utils" local data_types = require "st.zigbee.data_types" From 39a2d7385b3a2e18467590c19d85995e6ecfb2ac Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:29 -0600 Subject: [PATCH 318/449] CHAD-17072: zigbee-range-extender lazy load subdrivers --- .../src/frient/can_handle.lua | 11 ++++++++++ .../zigbee-range-extender/src/frient/init.lua | 22 +++++-------------- .../zigbee-range-extender/src/init.lua | 20 ++++------------- .../src/lazy_load_subdriver.lua | 15 +++++++++++++ .../zigbee-range-extender/src/sub_drivers.lua | 8 +++++++ .../test_frient_zigbee_range_extender.lua | 15 ++----------- .../src/test/test_zigbee_extend.lua | 15 ++----------- 7 files changed, 47 insertions(+), 59 deletions(-) create mode 100644 drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua new file mode 100644 index 0000000000..9967c0edf8 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua index f4a754bfeb..fef5d8470a 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -70,9 +60,7 @@ local frient_range_extender = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111") - end + can_handle = require("frient.can_handle"), } -return frient_range_extender \ No newline at end of file +return frient_range_extender diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index 2523a3d45e..565aa74a28 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local defaults = require "st.zigbee.defaults" @@ -32,9 +22,7 @@ local zigbee_range_driver_template = { } }, health_check = false, - sub_drivers = { - require("frient") - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua b/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua new file mode 100644 index 0000000000..2f30e461ee --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("frient"), +} +return sub_drivers 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 8f63df1705..4aa03d2bca 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 39404005fa..c76e228cef 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From b15a865061b88f614eeba1d923eb22113251f80d Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 8 Dec 2025 17:07:06 -0800 Subject: [PATCH 319/449] WWSTCERT-9352 Ikea Bilresa --- 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 a13168ac8b..786bfe997e 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -547,6 +547,12 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0633 deviceProfileName: plug-binary +#Ikea + - id: "4476/32769" + deviceLabel: BILRESA dual button + vendorId: 0x117C + productId: 0x8001 + deviceProfileName: 2-button-battery #Innovation Matters - id: "4978/1" deviceLabel: M2D Bridge From a80097c016c1809558a4f549b53bcd82a38a717c Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 8 Dec 2025 17:03:10 -0800 Subject: [PATCH 320/449] WWSTCERT-9343 WWSTCERT-9346 WWSTCERT-9349 WWSTCERT-9355 WWSTCERT-9358 --- .../matter-sensor/fingerprints.yml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 8836c276cd..7128bf9917 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -84,6 +84,32 @@ matterManufacturer: vendorId: 0x120B productId: 0x1060 deviceProfileName: temperature-humidity-battery + # Ikea + - id: "4476/32773" + deviceLabel: TIMMERFLOTTE temp/hmd sensor + vendorId: 0x117C + productId: 0x8005 + deviceProfileName: temperature-humidity-battery + - id: "4476/32774" + deviceLabel: KLIPPBOK Matter water leak sensor smart + vendorId: 0x117C + productId: 0x8006 + deviceProfileName: leak-battery + - id: "4476/12288" + deviceLabel: MYGGSPRAY wrlss mtn sensor + vendorId: 0x117C + productId: 0x3000 + deviceProfileName: matter-motion-battery-illuminance + - id: "4476/12289" + deviceLabel: ALPSTUGA Matter air quality sensor smart + vendorId: 0x117C + productId: 0x3001 + deviceProfileName: aqs-modular-temp-humidity + - id: "4476/32775" + deviceLabel: MYGGBETT Matter door/window sensor smart + vendorId: 0x117C + productId: 0x8007 + deviceProfileName: contact-battery # Legrand - id: "Legrand/Netatmo/Smart-2-in-1-Sensor" deviceLabel: Netatmo Smart 2-in-1 Sensor From 684579f3677ff65d2f38a7794ed79e4ecc77ce7c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Tue, 9 Dec 2025 14:48:30 -0600 Subject: [PATCH 321/449] add update modular profile field --- .../air_quality_sensor_utils/device_configuration.lua | 1 + .../air_quality_sensor/air_quality_sensor_utils/fields.lua | 2 ++ .../matter-sensor/src/sub_drivers/air_quality_sensor/init.lua | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) 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..6e1f1a5d81 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 @@ -71,6 +71,7 @@ function DeviceConfiguration.match_profile(device) end device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + device:set_field(fields.MODULAR_PROFILE_UPDATED, true) -- 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. 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..0d3c5dd21c 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 @@ -26,6 +26,8 @@ local AirQualitySensorFields = {} AirQualitySensorFields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C +AirQualitySensorFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" + AirQualitySensorFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" AirQualitySensorFields.units_required = { 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 869f2d337b..a0427ac665 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 @@ -66,11 +66,12 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) end function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then + if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) 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) end + device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) aqs_utils.set_supported_health_concern_values(device) device:subscribe() end From 82892a177347916b942b48d677e4961c3064e4d4 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 2 Dec 2025 13:03:01 -0600 Subject: [PATCH 322/449] CHAD-17043: Created pre-commit script to check for newline and copyright on all staged lua files --- tools/pre-commit | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 tools/pre-commit diff --git a/tools/pre-commit b/tools/pre-commit new file mode 100755 index 0000000000..160ed72816 --- /dev/null +++ b/tools/pre-commit @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# +# pre-commit +# +# - Copy to .git/hooks/pre-commit +# - Ensure executable + +set -e + +year=$(date +%Y) +copyright_header="""-- Copyright $year SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0""" + +# Get changed files that are Added or Modified +# files=$(find drivers/SmartThings/zigbee-motion-sensor/src/ -type f -follow -print) +files=$(git diff --staged --name-only --diff-filter=AM) + +fail=0 +for file in $files; do + # Only process .lua files + case "$file" in + *.lua) + # Skip if not a regular file + [ -f "$file" ] || continue + + # Check if file is non-empty and does NOT contain the current year copyright header + header_text=$(head -n 3 $file) + if [ -s "$file" ] && [[ "$header_text" != "$copyright_header" ]]; then + echo "$file: Incorrect copyright header" + fail=1 + fi + + # Check if file is non-empty and does NOT end with a newline + if [ -s "$file" ] && [ -n "$(tail -c 1 "$file")" ]; then + echo "$file: Missing newline at end of file" + fail=1 + fi + ;; + esac +done + +exit $fail \ No newline at end of file From ad652120149c13a5a574eb15f07b042d2711c192 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 2 Dec 2025 13:10:52 -0600 Subject: [PATCH 323/449] CHAD-17043: Removed testing line --- tools/pre-commit | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/pre-commit b/tools/pre-commit index 160ed72816..74c8ac3d29 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -12,7 +12,6 @@ copyright_header="""-- Copyright $year SmartThings, Inc. -- Licensed under the Apache License, Version 2.0""" # Get changed files that are Added or Modified -# files=$(find drivers/SmartThings/zigbee-motion-sensor/src/ -type f -follow -print) files=$(git diff --staged --name-only --diff-filter=AM) fail=0 From 9eca47dba0ca5a21d41f3047fdfba68afc4d0cb6 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 2 Dec 2025 14:43:15 -0600 Subject: [PATCH 324/449] CHAD-17043: Copyright check no longer strict on year --- tools/pre-commit | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tools/pre-commit b/tools/pre-commit index 74c8ac3d29..1a848a7668 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -1,5 +1,8 @@ #!/usr/bin/env bash # +# Copyright 2025 SmartThings, Inc. +# Licensed under the Apache License, Version 2.0 +# # pre-commit # # - Copy to .git/hooks/pre-commit @@ -7,10 +10,6 @@ set -e -year=$(date +%Y) -copyright_header="""-- Copyright $year SmartThings, Inc. --- Licensed under the Apache License, Version 2.0""" - # Get changed files that are Added or Modified files=$(git diff --staged --name-only --diff-filter=AM) @@ -22,10 +21,9 @@ for file in $files; do # Skip if not a regular file [ -f "$file" ] || continue - # Check if file is non-empty and does NOT contain the current year copyright header - header_text=$(head -n 3 $file) - if [ -s "$file" ] && [[ "$header_text" != "$copyright_header" ]]; then - echo "$file: Incorrect copyright header" + # Check if file is non-empty and does NOT contain the copyright header + if [ -s "$file" ] && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4} SmartThings") ]]; then + echo "$file: SmartThings Copyright missing from file" fail=1 fi From 799d8466c8bf3f4299163291be50a6fd69da28a2 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 2 Dec 2025 15:42:14 -0600 Subject: [PATCH 325/449] CHAD-17043: Allow for range of years in copyright checking --- tools/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pre-commit b/tools/pre-commit index 1a848a7668..9f6bbc474d 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -22,7 +22,7 @@ for file in $files; do [ -f "$file" ] || continue # Check if file is non-empty and does NOT contain the copyright header - if [ -s "$file" ] && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4} SmartThings") ]]; then + if [ -s "$file" ] && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4}(?:-\d{4})? SmartThings") ]]; then echo "$file: SmartThings Copyright missing from file" fail=1 fi From 9ce9c5811316b3d78bfdf12f57a4d7fd5188bb7a Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 3 Dec 2025 13:15:48 -0600 Subject: [PATCH 326/449] CHAD-17043: Newline and SmartThings driver checking --- tools/pre-commit | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/pre-commit b/tools/pre-commit index 9f6bbc474d..fd84647450 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -21,8 +21,9 @@ for file in $files; do # Skip if not a regular file [ -f "$file" ] || continue - # Check if file is non-empty and does NOT contain the copyright header - if [ -s "$file" ] && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4}(?:-\d{4})? SmartThings") ]]; then + # Check if file is not empty, is a SmartThings driver and has the copyright header + if [ -s "$file" ] && [[ "$file" =~ "drivers/SmartThings" ]] + && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4}(?:-\d{4})? SmartThings") ]]; then echo "$file: SmartThings Copyright missing from file" fail=1 fi @@ -36,4 +37,4 @@ for file in $files; do esac done -exit $fail \ No newline at end of file +exit $fail From 660eb54857406b938460ca40ed3dc1b1470e9019 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 3 Dec 2025 13:20:39 -0600 Subject: [PATCH 327/449] CHAD-17043: Ensure executable commited and updated to soft linking logic --- tools/pre-commit | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/pre-commit b/tools/pre-commit index fd84647450..29c4f7b56d 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -5,7 +5,8 @@ # # pre-commit # -# - Copy to .git/hooks/pre-commit +# - Soft link into .git/hooks/pre-commit +# - $ ln -s tools/pre-commit .git/hooks/pre-commit # - Ensure executable set -e From ed4e8cd40039b9c505ed417204b057a2b01e6a69 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 10 Dec 2025 13:29:30 -0600 Subject: [PATCH 328/449] CHAD-17043: Updates and fixes - Soft linking note fix - Command line arg for running script on a folder - Only checking for copyright in files in SmartThings driver folder --- tools/pre-commit | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/pre-commit b/tools/pre-commit index 29c4f7b56d..c9bc4d4d4f 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -6,13 +6,17 @@ # pre-commit # # - Soft link into .git/hooks/pre-commit -# - $ ln -s tools/pre-commit .git/hooks/pre-commit +# - $ ln -s ../../tools/pre-commit .git/hooks/pre-commit # - Ensure executable set -e # Get changed files that are Added or Modified -files=$(git diff --staged --name-only --diff-filter=AM) +if [[ "$1" ]]; then + files=$(find $1 -type f -follow -print) +else + files=$(git diff --staged --name-only --diff-filter=AM) +fi fail=0 for file in $files; do @@ -23,7 +27,7 @@ for file in $files; do [ -f "$file" ] || continue # Check if file is not empty, is a SmartThings driver and has the copyright header - if [ -s "$file" ] && [[ "$file" =~ "drivers/SmartThings" ]] + if [ -s "$file" ] && [[ "$file" =~ "drivers/SmartThings" ]] \ && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4}(?:-\d{4})? SmartThings") ]]; then echo "$file: SmartThings Copyright missing from file" fail=1 From 6ca2f11fba0a0918176fe6971f2c2a4ce79f0f26 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:14 -0600 Subject: [PATCH 329/449] CHAD-17075: zigbee-smoke-detector lazy load subdrivers --- .../src/aqara-gas/can_handle.lua | 14 +++++++++ .../src/aqara-gas/fingerprints.lua | 8 +++++ .../src/aqara-gas/init.lua | 30 +++---------------- .../src/aqara/can_handle.lua | 14 +++++++++ .../src/aqara/fingerprints.lua | 8 +++++ .../zigbee-smoke-detector/src/aqara/init.lua | 30 +++---------------- .../src/frient/can_handle.lua | 11 +++++++ .../zigbee-smoke-detector/src/frient/init.lua | 20 +++---------- .../zigbee-smoke-detector/src/init.lua | 24 ++++----------- .../src/lazy_load_subdriver.lua | 15 ++++++++++ .../zigbee-smoke-detector/src/sub_drivers.lua | 10 +++++++ .../src/test/test_aqara_gas_detector.lua | 15 ++-------- .../src/test/test_aqara_smoke_detector.lua | 15 ++-------- .../src/test/test_frient_heat_detector.lua | 17 ++--------- .../src/test/test_frient_smoke_detector.lua | 15 ++-------- .../src/test/test_zigbee_smoke_detector.lua | 15 ++-------- 16 files changed, 108 insertions(+), 153 deletions(-) create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua new file mode 100644 index 0000000000..41ba568b80 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara-gas.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara-gas") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua new file mode 100644 index 0000000000..61cc4d9730 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_gas.acn02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua index 297701272d..e0474243ce 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara-gas/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" local capabilities = require "st.capabilities" @@ -31,9 +21,6 @@ local PRIVATE_LIFE_TIME_ATTRIBUTE_ID = 0x0128 local PRIVATE_GAS_ZONE_STATUS_ATTRIBUTE_ID = 0x013A -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_gas.acn02" } -} local CONFIGURATIONS = { @@ -125,14 +112,6 @@ local function self_check_attr_handler(self, device, zone_status, zb_rx) PRIVATE_CLUSTER_ID, PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true)) end -local function is_aqara_products(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_init(driver, device) if CONFIGURATIONS ~= nil then @@ -186,8 +165,7 @@ local aqara_gas_detector_handler = { [startSelfCheckCommandName] = self_check_attr_handler }, }, - can_handle = is_aqara_products + can_handle = require("aqara-gas.can_handle"), } return aqara_gas_detector_handler - diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..d296aa8518 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_smoke.acn03" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua index 91aba0a5e9..1950d1f923 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/aqara/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -30,9 +20,6 @@ local PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID = 0x013A local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_smoke.acn03" } -} local CONFIGURATIONS = { @@ -98,14 +85,6 @@ local function self_check_attr_handler(self, device, zone_status, zb_rx) PRIVATE_CLUSTER_ID, PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true)) end -local function is_aqara_products(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_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) @@ -154,8 +133,7 @@ local aqara_gas_detector_handler = { [startSelfCheckCommandName] = self_check_attr_handler }, }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_gas_detector_handler - diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua new file mode 100644 index 0000000000..5154ec5fe6 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "SMSZB-120" or device:get_model() == "HESZB-120") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua index 1d6d85311a..d3a3604c26 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local capabilities = require "st.capabilities" @@ -270,8 +260,6 @@ local frient_smoke_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SMSZB-120" or device:get_model() == "HESZB-120") - end + can_handle = require("frient.can_handle"), } return frient_smoke_sensor diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua index 941c3312da..fe64260c11 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -25,11 +15,7 @@ local zigbee_smoke_driver_template = { capabilities.temperatureMeasurement, capabilities.temperatureAlarm }, - sub_drivers = { - require("frient"), - require("aqara-gas"), - require("aqara") - }, + sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, } @@ -37,4 +23,4 @@ local zigbee_smoke_driver_template = { defaults.register_for_default_handlers(zigbee_smoke_driver_template, zigbee_smoke_driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) local zigbee_smoke_driver = ZigbeeDriver("zigbee-smoke-detector", zigbee_smoke_driver_template) -zigbee_smoke_driver:run() \ No newline at end of file +zigbee_smoke_driver:run() diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua new file mode 100644 index 0000000000..e6c5e004f3 --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("frient"), + lazy_load_if_possible("aqara-gas"), + lazy_load_if_possible("aqara"), +} +return sub_drivers 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 6c194351a6..4c1dcb1552 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 bd0fae534c..1af67b36cd 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 aec09a8435..364aae57a0 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -580,4 +569,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 574f08ae76..fa012a321b 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 00d0b43c10..b27713fa19 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From eb5724a8a4aa32030f85082c9769efaafe3e869b Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:21 -0600 Subject: [PATCH 330/449] CHAD-17040: zwave-thermostat: Subdriver modifications for lazy loading --- .../aeotec-radiator-thermostat/can_handle.lua | 14 ++++++++ .../src/aeotec-radiator-thermostat/init.lua | 24 +++----------- .../src/apiv6_bugfix/can_handle.lua | 25 ++++++++++++++ .../src/apiv6_bugfix/fingerprints.lua | 9 +++++ .../src/apiv6_bugfix/init.lua | 22 +++---------- .../src/ct100-thermostat/can_handle.lua | 15 +++++++++ .../src/ct100-thermostat/fingerprints.lua | 9 +++++ .../src/ct100-thermostat/init.lua | 33 +++---------------- .../src/fibaro-heat-controller/can_handle.lua | 15 +++++++++ .../fibaro-heat-controller/fingerprints.lua | 9 +++++ .../src/fibaro-heat-controller/init.lua | 31 +++-------------- .../SmartThings/zwave-thermostat/src/init.lua | 27 +++------------ .../src/lazy_load_subdriver.lua | 18 ++++++++++ .../popp-radiator-thermostat/can_handle.lua | 14 ++++++++ .../src/popp-radiator-thermostat/init.lua | 24 +++----------- .../qubino-flush-thermostat/can_handle.lua | 14 ++++++++ .../qubino-flush-thermostat/fingerprints.lua | 8 +++++ .../src/qubino-flush-thermostat/init.lua | 29 +++------------- .../src/stelpro-ki-thermostat/can_handle.lua | 15 +++++++++ .../stelpro-ki-thermostat/fingerprints.lua | 8 +++++ .../src/stelpro-ki-thermostat/init.lua | 30 +++-------------- .../zwave-thermostat/src/sub_drivers.lua | 15 +++++++++ .../thermostat-heating-battery/can_handle.lua | 15 +++++++++ .../fingerprints.lua | 9 +++++ .../src/thermostat-heating-battery/init.lua | 33 +++---------------- 25 files changed, 248 insertions(+), 217 deletions(-) create mode 100644 drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua create mode 100644 drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua diff --git a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua new file mode 100644 index 0000000000..9feb63e1fa --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_radiator_thermostat(opts, driver, device, ...) + local AEOTEC_THERMOSTAT_FINGERPRINT = {mfr = 0x0371, prod = 0x0002, model = 0x0015} + + if device:id_match(AEOTEC_THERMOSTAT_FINGERPRINT.mfr, AEOTEC_THERMOSTAT_FINGERPRINT.prod, AEOTEC_THERMOSTAT_FINGERPRINT.model) then + return true, require "aeotec-radiator-thermostat" + else + return false + end +end + +return can_handle_aeotec_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua index b101a43d38..cdb90e7f44 100755 --- a/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/aeotec-radiator-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,12 +14,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ver --- @type st.zwave.CommandClass.ThermostatSetpoint local ThermostatSetpoint = (require "st.zwave.CommandClass.ThermostatSetpoint")({version=1}) -local AEOTEC_THERMOSTAT_FINGERPRINT = {mfr = 0x0371, prod = 0x0002, model = 0x0015} - -local function can_handle_aeotec_radiator_thermostat(opts, driver, device, ...) - return device:id_match(AEOTEC_THERMOSTAT_FINGERPRINT.mfr, AEOTEC_THERMOSTAT_FINGERPRINT.prod, AEOTEC_THERMOSTAT_FINGERPRINT.model) -end - local function thermostat_mode_report_handler(self, device, cmd) local event = nil if (cmd.args.mode == ThermostatMode.mode.OFF) then @@ -114,7 +98,7 @@ local aeotec_radiator_thermostat = { } }, lifecycle_handlers = {added = device_added}, - can_handle = can_handle_aeotec_radiator_thermostat + can_handle = require("aeotec-radiator-thermostat.can_handle"), } return aeotec_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..079eeee5d3 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,25 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local version = require "version" + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local DANFOSS_LC13_THERMOSTAT_FPS = require "apiv6_bugfix.fingerprints" + + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION and not + (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, + DANFOSS_LC13_THERMOSTAT_FPS[1].productType, + DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or + device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, + DANFOSS_LC13_THERMOSTAT_FPS[2].productType, + DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) then + return true, require "apiv6_bugfix" + else + return false + end +end + +return can_handle diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua new file mode 100644 index 0000000000..e87a5990e2 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_LC13_THERMOSTAT_FPS = { + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat +} + +return DANFOSS_LC13_THERMOSTAT_FPS diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua index 4994710970..52c419a590 100644 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua @@ -1,23 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local DANFOSS_LC13_THERMOSTAT_FPS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION and not - (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[1].productType, - DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or - device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[2].productType, - DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) -end local function wakeup_notification(driver, device, cmd) device:refresh() @@ -30,7 +16,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua new file mode 100644 index 0000000000..6483fc2393 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ct100_thermostat(opts, driver, device) + local CT100_THERMOSTAT_FINGERPRINTS = require "ct100-thermostat.fingerprints" + for _, fingerprint in ipairs(CT100_THERMOSTAT_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require "ct100-thermostat" + end + end + + return false +end + +return can_handle_ct100_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua new file mode 100644 index 0000000000..c45eb5e7af --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CT100_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0098, productType = 0x6401, productId = 0x0107 }, -- 2Gig CT100 Programmable Thermostat + { manufacturerId = 0x0098, productType = 0x6501, productId = 0x000C }, -- Iris Thermostat +} + +return CT100_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua index e44da0ca36..d5859fe777 100644 --- a/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/ct100-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -33,11 +23,6 @@ local cooling_setpoint_defaults = require "st.zwave.defaults.thermostatCoolingSe local constants = require "st.zwave.constants" local utils = require "st.utils" -local CT100_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0098, productType = 0x6401, productId = 0x0107 }, -- 2Gig CT100 Programmable Thermostat - { manufacturerId = 0x0098, productType = 0x6501, productId = 0x000C }, -- Iris Thermostat -} - -- This old device uses separate endpoints to get values of temp and humidity -- DTH actually uses the old mutliInstance encap, but multichannel should be back-compat local TEMPERATURE_ENDPOINT = 1 @@ -75,16 +60,6 @@ local function set_setpoint_factory(setpoint_type) end end -local function can_handle_ct100_thermostat(opts, driver, device) - for _, fingerprint in ipairs(CT100_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - - return false -end - local function thermostat_mode_report_handler(self, device, cmd) local event = nil @@ -210,7 +185,7 @@ local ct100_thermostat = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = can_handle_ct100_thermostat, + can_handle = require("ct100-thermostat.can_handle"), } return ct100_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua new file mode 100644 index 0000000000..6ece30f897 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_heat_controller(opts, driver, device, ...) + local FINGERPRINTS = require("fibaro-heat-controller.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("fibaro-heat-controller") + end + end + + return false +end + +return can_handle_fibaro_heat_controller diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua new file mode 100644 index 0000000000..31cbd7ff0e --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_HEAT_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1301, model = 0x1000}, -- Fibaro Heat Controller + {mfr = 0x010F, prod = 0x1301, model = 0x1001} -- Fibaro Heat Controller +} + +return FIBARO_HEAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua index be5365edf0..ab7ef51f30 100644 --- a/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/fibaro-heat-controller/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -30,22 +20,9 @@ local ApplicationStatus = (require "st.zwave.CommandClass.ApplicationStatus")({v local utils = require "st.utils" -local FIBARO_HEAT_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1301, model = 0x1000}, -- Fibaro Heat Controller - {mfr = 0x010F, prod = 0x1301, model = 0x1001} -- Fibaro Heat Controller -} local FORCED_REFRESH_THREAD = "forcedRefreshThread" -local function can_handle_fibaro_heat_controller(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_HEAT_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - - return false -end local function thermostat_mode_report_handler(self, device, cmd) local event = nil @@ -189,7 +166,7 @@ local fibaro_heat_controller = { added = device_added, init = map_components }, - can_handle = can_handle_fibaro_heat_controller + can_handle = require("fibaro-heat-controller.can_handle"), } return fibaro_heat_controller diff --git a/drivers/SmartThings/zwave-thermostat/src/init.lua b/drivers/SmartThings/zwave-thermostat/src/init.lua index 4fb3eff09e..3668085b1a 100755 --- a/drivers/SmartThings/zwave-thermostat/src/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.Driver @@ -115,16 +105,7 @@ local driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("aeotec-radiator-thermostat"), - require("popp-radiator-thermostat"), - require("ct100-thermostat"), - require("fibaro-heat-controller"), - require("stelpro-ki-thermostat"), - require("qubino-flush-thermostat"), - require("thermostat-heating-battery"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_attrs_enabled = true}) diff --git a/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua new file mode 100644 index 0000000000..19e5f39cf3 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_popp_radiator_thermostat(opts, driver, device, ...) + local POPP_THERMOSTAT_FINGERPRINT = {mfr = 0x0002, prod = 0x0115, model = 0xA010} + + if device:id_match(POPP_THERMOSTAT_FINGERPRINT.mfr, POPP_THERMOSTAT_FINGERPRINT.prod, POPP_THERMOSTAT_FINGERPRINT.model) then + return true, require "popp-radiator-thermostat" + else + return false + end +end + +return can_handle_popp_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua index f6ba9955b0..46234ea2ac 100755 --- a/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/popp-radiator-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -29,8 +19,6 @@ local LATEST_WAKEUP = "latest_wakeup" local CACHED_SETPOINT = "cached_setpoint" local POPP_WAKEUP_INTERVAL = 600 --seconds -local POPP_THERMOSTAT_FINGERPRINT = {mfr = 0x0002, prod = 0x0115, model = 0xA010} - local function get_latest_wakeup_timestamp(device) return device:get_field(LATEST_WAKEUP) end @@ -48,10 +36,6 @@ local function seconds_since_latest_wakeup(device) end end -local function can_handle_popp_radiator_thermostat(opts, driver, device, ...) - return device:id_match(POPP_THERMOSTAT_FINGERPRINT.mfr, POPP_THERMOSTAT_FINGERPRINT.prod, POPP_THERMOSTAT_FINGERPRINT.model) -end - -- POPP is a sleepy device, therefore it won't accept setpoint commands rightaway. -- That's why driver waits for a device to wake up and then sends cached setpoint command. -- Driver assumes that wakeUps come in reguraly every 10 minutes. @@ -116,7 +100,7 @@ local popp_radiator_thermostat = { lifecycle_handlers = { added = added_handler }, - can_handle = can_handle_popp_radiator_thermostat + can_handle = require("popp-radiator-thermostat.can_handle"), } return popp_radiator_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua new file mode 100644 index 0000000000..e5a6a8474b --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_thermostat(opts, driver, device, ...) + local FINGERPRINTS = require("qubino-flush-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-flush-thermostat") + end + end + return false +end + +return can_handle_qubino_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua new file mode 100644 index 0000000000..d640f7e1ce --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0005, model = 0x0054}, -- Qubino Flush On/Off Thermostat 2 +} + +return QUBINO_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua index d80e58e2da..fd9ff2aacb 100644 --- a/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/qubino-flush-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.defaults.switch local TemperatureMeasurementDefaults = require "st.zwave.defaults.temperatureMeasurement" @@ -31,9 +21,6 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.ThermostatOperatingState local ThermostatOperatingState = (require "st.zwave.CommandClass.ThermostatOperatingState")({version=1}) -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0005, model = 0x0054}, -- Qubino Flush On/Off Thermostat 2 -} -- parameter which tells whether device is configured heat or cool thermostat mode local DEVICE_MODE_PARAMETER = 59 @@ -47,14 +34,6 @@ local CONFIGURED_MODE = "configured_mode" local COOL_MODE = "cool" local HEAT_MODE = "heat" -local function can_handle_qubino_thermostat(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function info_changed(self, device, event, args) local new_parameter_value @@ -137,7 +116,7 @@ local qubino_thermostat = { infoChanged = info_changed }, NAME = "qubino thermostat", - can_handle = can_handle_qubino_thermostat + can_handle = require("qubino-flush-thermostat.can_handle"), } return qubino_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua new file mode 100644 index 0000000000..65af08df62 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_stelpro_ki_thermostat(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("stelpro-ki-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("stelpro-ki-thermostat") + end + end + + return false +end + +return can_handle_stelpro_ki_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua new file mode 100644 index 0000000000..00a43a739f --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_KI_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0239, productType = 0x0001, productId = 0x0001 } -- Stelpro Ki Thermostat +} + +return STELPRO_KI_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua index 3520e35325..7b6acbe3e7 100644 --- a/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/stelpro-ki-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local log = require "log" local capabilities = require "st.capabilities" @@ -21,19 +11,7 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.zwave.CommandClass.ThermostatMode local ThermostatMode = (require "st.zwave.CommandClass.ThermostatMode")({ version = 2 }) -local STELPRO_KI_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0239, productType = 0x0001, productId = 0x0001 } -- Stelpro Ki Thermostat -} - -local function can_handle_stelpro_ki_thermostat(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(STELPRO_KI_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function sensor_multilevel_report_handler(self, device, cmd) if (cmd.args.sensor_type == SensorMultilevel.sensor_type.TEMPERATURE) then @@ -138,7 +116,7 @@ local stelpro_ki_thermostat = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_stelpro_ki_thermostat, + can_handle = require("stelpro-ki-thermostat.can_handle"), } return stelpro_ki_thermostat diff --git a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua new file mode 100644 index 0000000000..dc30091b79 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua @@ -0,0 +1,15 @@ +-- 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("aeotec-radiator-thermostat"), + lazy_load_if_possible("popp-radiator-thermostat"), + lazy_load_if_possible("ct100-thermostat"), + lazy_load_if_possible("fibaro-heat-controller"), + lazy_load_if_possible("stelpro-ki-thermostat"), + lazy_load_if_possible("qubino-flush-thermostat"), + lazy_load_if_possible("thermostat-heating-battery"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua new file mode 100644 index 0000000000..f02798191a --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_thermostat_heating_battery(opts, driver, device, cmd, ...) + local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = require "thermostat-heating-battery.fingerprints" + for _, fingerprint in ipairs(DANFOSS_LC13_THERMOSTAT_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require "thermostat-heating-battery" + end + end + + return false +end + +return can_handle_thermostat_heating_battery diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua new file mode 100644 index 0000000000..bc32cac629 --- /dev/null +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = { + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat + { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat +} + +return DANFOSS_LC13_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua index 4cf3c22f24..60a9a2055f 100644 --- a/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua +++ b/drivers/SmartThings/zwave-thermostat/src/thermostat-heating-battery/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -42,11 +32,6 @@ local CLAMP = { CELSIUS_MAX = 28 } -local DANFOSS_LC13_THERMOSTAT_FINGERPRINTS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - local WEEK = {6, 0, 1, 2, 3, 4, 5} --[[ Danfoss LC13 (Living Connect) @@ -62,16 +47,6 @@ Note: https://idency.com/products/idencyhome/smarthome/sensors/danfoss-z-wave-li to the Z-Wave network, it only allows a one-way communication to change its setpoint. --]] -local function can_handle_thermostat_heating_battery(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(DANFOSS_LC13_THERMOSTAT_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - - return false -end - local function adjust_temperature_if_exceeded_min_max_limit (degree, scale) if scale == ThermostatSetpoint.scale.CELSIUS then return utils.clamp_value(degree, CLAMP.CELSIUS_MIN, CLAMP.CELSIUS_MAX) @@ -283,7 +258,7 @@ local thermostat_heating_battery = { init = device_init, added = added_handler, }, - can_handle = can_handle_thermostat_heating_battery + can_handle = require("thermostat-heating-battery.can_handle"), } return thermostat_heating_battery From c9b317e640654b4449f491931a9d615e5830be1a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 10 Dec 2025 13:35:14 -0800 Subject: [PATCH 331/449] fixup --- drivers/SmartThings/matter-sensor/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 7128bf9917..0db00e11d6 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -99,7 +99,7 @@ matterManufacturer: deviceLabel: MYGGSPRAY wrlss mtn sensor vendorId: 0x117C productId: 0x3000 - deviceProfileName: matter-motion-battery-illuminance + deviceProfileName: motion-illuminance-battery - id: "4476/12289" deviceLabel: ALPSTUGA Matter air quality sensor smart vendorId: 0x117C From 6f354e736615d5907b66227d1c6c7b6b045574bf Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 10 Dec 2025 15:21:45 -0800 Subject: [PATCH 332/449] WWSTCERT-9423 second fingerprint --- 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 786bfe997e..9c96a5666d 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -548,6 +548,11 @@ matterManufacturer: productId: 0x0633 deviceProfileName: plug-binary #Ikea + - id: "4476/32768" + deviceLabel: BILRESA scroll wheel + vendorId: 0x117C + productId: 0x8000 + deviceProfileName: 2-button-battery - id: "4476/32769" deviceLabel: BILRESA dual button vendorId: 0x117C From 7235ce7b6c3b51710fddf61a4e830069cb5da2a5 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:23 -0600 Subject: [PATCH 333/449] CHAD-17081: zigbee-watering-kit lazy loading of sub-drivers --- .../SmartThings/zigbee-watering-kit/src/init.lua | 7 ++++--- .../src/lazy_load_subdriver.lua | 15 +++++++++++++++ .../zigbee-watering-kit/src/sub_drivers.lua | 8 ++++++++ .../src/thirdreality/can_handle.lua | 11 +++++++++++ .../zigbee-watering-kit/src/thirdreality/init.lua | 7 ++++--- 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua diff --git a/drivers/SmartThings/zigbee-watering-kit/src/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/init.lua index f39c04beaa..7dd35e6f09 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -10,9 +13,7 @@ local zigbee_water_driver_template = { capabilities.fanSpeed, capabilities.mode }, - sub_drivers = { - require("thirdreality") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua b/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua new file mode 100644 index 0000000000..e33b31c978 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("thirdreality"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..12afaa7d98 --- /dev/null +++ b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RWK0148Z" then + return true, require("thirdreality") + end + return false +end + +return thirdreality_can_handle diff --git a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua index 5f73f3f9ca..5e36e15624 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/thirdreality/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local IASZone = clusters.IASZone @@ -97,9 +100,7 @@ local thirdreality_device_handler = { lifecycle_handlers = { added = device_added }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RWK0148Z" - end + can_handle = require("thirdreality.can_handle"), } return thirdreality_device_handler From 9278b2bc4427fd886967ab0feda1f93714ab9e61 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:38 -0600 Subject: [PATCH 334/449] CHAD-17084: zwave-bulb lazy loading of sub-drivers --- .../src/aeon-multiwhite-bulb/can_handle.lua | 14 ++++++++ .../src/aeon-multiwhite-bulb/fingerprints.lua | 10 ++++++ .../src/aeon-multiwhite-bulb/init.lua | 31 +++-------------- .../src/aeotec-led-bulb-6/can_handle.lua | 23 +++++++++++++ .../zwave-bulb/src/aeotec-led-bulb-6/init.lua | 34 +++---------------- .../src/fibaro-rgbw-controller/can_handle.lua | 20 +++++++++++ .../src/fibaro-rgbw-controller/init.lua | 31 +++-------------- drivers/SmartThings/zwave-bulb/src/init.lua | 22 +++--------- .../zwave-bulb/src/lazy_load_subdriver.lua | 18 ++++++++++ .../zwave-bulb/src/sub_drivers.lua | 10 ++++++ .../src/test/test_aeon_multiwhite_bulb.lua | 16 ++------- .../src/test/test_aeotec_led_bulb_6.lua | 16 ++------- .../src/test/test_fibaro_rgbw_controller.lua | 16 ++------- .../zwave-bulb/src/test/test_zwave_bulb.lua | 16 ++------- 14 files changed, 123 insertions(+), 154 deletions(-) create mode 100644 drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua create mode 100644 drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua create mode 100644 drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua create mode 100644 drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-bulb/src/sub_drivers.lua diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua new file mode 100644 index 0000000000..6ff48d2fe2 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_multiwhite_bulb(opts, driver, device, ...) + local FINGERPRINTS = require("aeon-multiwhite-bulb.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeon-multiwhite-bulb") + end + end + return false +end + +return can_handle_aeon_multiwhite_bulb diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua new file mode 100644 index 0000000000..6ec474efa9 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEON_MULTIWHITE_BULB_FINGERPRINTS = { + {mfr = 0x0371, prod = 0x0103, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White US + {mfr = 0x0371, prod = 0x0003, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White EU + {mfr = 0x0300, prod = 0x0003, model = 0x0004} -- ilumin Tunable White +} + +return AEON_MULTIWHITE_BULB_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua index 9907fefbb5..4a9cc277ca 100644 --- a/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/aeon-multiwhite-bulb/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.utils @@ -26,25 +16,12 @@ local SwitchColor = (require "st.zwave.CommandClass.SwitchColor")({ version = 3 --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version = 4 }) -local AEON_MULTIWHITE_BULB_FINGERPRINTS = { - {mfr = 0x0371, prod = 0x0103, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White US - {mfr = 0x0371, prod = 0x0003, model = 0x0001}, -- Aeon LED Bulb 6 Multi-White EU - {mfr = 0x0300, prod = 0x0003, model = 0x0004} -- ilumin Tunable White -} local WARM_WHITE_CONFIG = 0x51 local COLD_WHITE_CONFIG = 0x52 local SWITCH_COLOR_QUERY_DELAY = 2 local DEFAULT_COLOR_TEMPERATURE = 2700 -local function can_handle_aeon_multiwhite_bulb(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_MULTIWHITE_BULB_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function onoff_level_report_handler(self, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value @@ -126,7 +103,7 @@ local aeon_multiwhite_bulb = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature } }, - can_handle = can_handle_aeon_multiwhite_bulb, + can_handle = require("aeon-multiwhite-bulb.can_handle"), lifecycle_handlers = { added = device_added } diff --git a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua new file mode 100644 index 0000000000..ebeafc2c5c --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/can_handle.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is an Aeotec LED Bulb 6. +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device is an Aeotec LED Bulb 6, else false +local function is_aeotec_led_bulb_6(opts, driver, device, ...) + local AEOTEC_MFR_ID = 0x0371 + local AEOTEC_LED_BULB_6_PRODUCT_TYPE_US = 0x0103 + local AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU = 0x0003 + local AEOTEC_LED_BULB_6_PRODUCT_ID = 0x0002 + if device:id_match( + AEOTEC_MFR_ID, + { AEOTEC_LED_BULB_6_PRODUCT_TYPE_US, AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU }, + AEOTEC_LED_BULB_6_PRODUCT_ID) then + return true, require("aeotec-led-bulb-6") + end + return false +end + +return is_aeotec_led_bulb_6 diff --git a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua index 67755878db..e4886c5897 100644 --- a/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/aeotec-led-bulb-6/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.utils @@ -26,10 +16,6 @@ local SwitchColor = (require "st.zwave.CommandClass.SwitchColor")({ version=3 }) --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=4 }) -local AEOTEC_MFR_ID = 0x0371 -local AEOTEC_LED_BULB_6_PRODUCT_TYPE_US = 0x0103 -local AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU = 0x0003 -local AEOTEC_LED_BULB_6_PRODUCT_ID = 0x0002 local WARM_WHITE_CONFIG = 0x51 local COLD_WHITE_CONFIG = 0x52 @@ -102,18 +88,6 @@ function capability_handlers.refresh(driver, device) device:send(Configuration:Get({ parameter_number=COLD_WHITE_CONFIG })) end ---- Determine whether the passed device is an Aeotec LED Bulb 6. ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device is an Aeotec LED Bulb 6, else false -local function is_aeotec_led_bulb_6(opts, driver, device, ...) - return device:id_match( - AEOTEC_MFR_ID, - { AEOTEC_LED_BULB_6_PRODUCT_TYPE_US, AEOTEC_LED_BULB_6_PRODUCT_TYPE_EU }, - AEOTEC_LED_BULB_6_PRODUCT_ID) -end - local aeotec_led_bulb_6 = { NAME = "Aeotec LED Bulb 6", zwave_handlers = { @@ -129,7 +103,7 @@ local aeotec_led_bulb_6 = { [capabilities.refresh.commands.refresh.NAME] = capability_handlers.refresh } }, - can_handle = is_aeotec_led_bulb_6, + can_handle = require("aeotec-led-bulb-6.can_handle"), } return aeotec_led_bulb_6 diff --git a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua new file mode 100644 index 0000000000..aeef24b0ea --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_fibaro_rgbw_controller(opts, driver, device, ...) + local FIBARO_MFR_ID = 0x010F + local FIBARO_RGBW_CONTROLLER_PROD_TYPE = 0x0900 + local FIBARO_RGBW_CONTROLLER_PROD_ID_US = 0x2000 + local FIBARO_RGBW_CONTROLLER_PROD_ID_EU = 0x1000 + + if device:id_match( + FIBARO_MFR_ID, + FIBARO_RGBW_CONTROLLER_PROD_TYPE, + {FIBARO_RGBW_CONTROLLER_PROD_ID_US, FIBARO_RGBW_CONTROLLER_PROD_ID_EU} + ) then + return true, require("fibaro-rgbw-controller") + end + return false +end + +return is_fibaro_rgbw_controller diff --git a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua index 80dc8d4dc7..b83be97c23 100644 --- a/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/fibaro-rgbw-controller/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Association @@ -33,19 +23,6 @@ local CAP_CACHE_KEY = "st.capabilities." .. capabilities.colorControl.ID local LAST_COLOR_SWITCH_CMD_FIELD = "lastColorSwitchCmd" local FAKE_RGB_ENDPOINT = 10 -local FIBARO_MFR_ID = 0x010F -local FIBARO_RGBW_CONTROLLER_PROD_TYPE = 0x0900 -local FIBARO_RGBW_CONTROLLER_PROD_ID_US = 0x2000 -local FIBARO_RGBW_CONTROLLER_PROD_ID_EU = 0x1000 - -local function is_fibaro_rgbw_controller(opts, driver, device, ...) - return device:id_match( - FIBARO_MFR_ID, - FIBARO_RGBW_CONTROLLER_PROD_TYPE, - {FIBARO_RGBW_CONTROLLER_PROD_ID_US, FIBARO_RGBW_CONTROLLER_PROD_ID_EU} - ) -end - -- This handler is copied from defaults with scraped of sets for both WHITE channels local function set_color(driver, device, command) local r, g, b = utils.hsl_to_rgb(command.args.color.hue, command.args.color.saturation, command.args.color.lightness) @@ -201,7 +178,7 @@ local fibaro_rgbw_controller = { added = device_added, init = device_init }, - can_handle = is_fibaro_rgbw_controller, + can_handle = require("fibaro-rgbw-controller.can_handle"), } return fibaro_rgbw_controller diff --git a/drivers/SmartThings/zwave-bulb/src/init.lua b/drivers/SmartThings/zwave-bulb/src/init.lua index 62079594fa..58e2f32a7e 100644 --- a/drivers/SmartThings/zwave-bulb/src/init.lua +++ b/drivers/SmartThings/zwave-bulb/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.Driver @@ -30,11 +20,7 @@ local driver_template = { capabilities.colorTemperature, capabilities.powerMeter }, - sub_drivers = { - require("aeotec-led-bulb-6"), - require("aeon-multiwhite-bulb"), - require("fibaro-rgbw-controller") - }, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities, {native_capability_cmds_enabled = true}) diff --git a/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua b/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua new file mode 100644 index 0000000000..842e2056bd --- /dev/null +++ b/drivers/SmartThings/zwave-bulb/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("aeotec-led-bulb-6"), + lazy_load_if_possible("aeon-multiwhite-bulb"), + lazy_load_if_possible("fibaro-rgbw-controller"), +} +return sub_drivers 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 d6cdc044e0..7ae91bc853 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 0a1fa7e955..8757c4e11a 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 0fd3e98995..1610440aea 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 cf704bec2d..9d5e199927 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" From 440180b7a61b3f87dfc4ac68421effbe7c4ac358 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:15 -0600 Subject: [PATCH 335/449] CHAD-17086: zwave-electric-meter lazy loading of sub-drivers --- .../src/aeon-meter/can_handle.lua | 14 +++++++++ .../src/aeon-meter/fingerprints.lua | 9 ++++++ .../src/aeon-meter/init.lua | 31 ++----------------- .../src/aeotec-gen5-meter/can_handle.lua | 14 +++++++++ .../src/aeotec-gen5-meter/fingerprints.lua | 9 ++++++ .../src/aeotec-gen5-meter/init.lua | 31 ++----------------- .../zwave-electric-meter/src/init.lua | 22 +++---------- .../src/lazy_load_subdriver.lua | 18 +++++++++++ .../src/qubino-meter/can_handle.lua | 14 +++++++++ .../src/qubino-meter/fingerprints.lua | 9 ++++++ .../src/qubino-meter/init.lua | 30 +++--------------- .../zwave-electric-meter/src/sub_drivers.lua | 10 ++++++ .../src/test/test_aeon_meter.lua | 16 ++-------- .../src/test/test_aeotec_gen5_meter.lua | 16 ++-------- .../src/test/test_qubino_3_phase_meter.lua | 16 ++-------- .../src/test/test_qubino_smart_meter.lua | 16 ++-------- .../src/test/test_zwave_electric_meter.lua | 16 ++-------- 17 files changed, 126 insertions(+), 165 deletions(-) create mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua new file mode 100644 index 0000000000..a0bcfde376 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_meter(opts, driver, device, ...) + local FINGERPRINTS = require("aeon-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeon-meter") + end + end + return false +end + +return can_handle_aeon_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua new file mode 100644 index 0000000000..5970f40d6d --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEON_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0002, model = 0x0009}, -- DSB09xxx-ZWUS + {mfr = 0x0086, prod = 0x0002, model = 0x0001}, -- DSB28-ZWEU +} + +return AEON_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua index 9f5ec4bee4..334bbb71cd 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/aeon-meter/init.lua @@ -1,34 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEON_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0002, model = 0x0009}, -- DSB09xxx-ZWUS - {mfr = 0x0086, prod = 0x0002, model = 0x0001}, -- DSB28-ZWEU -} - -local function can_handle_aeon_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEON_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local do_configure = function (self, device) device:send(Configuration:Set({parameter_number = 101, size = 4, configuration_value = 4})) -- combined power in watts... device:send(Configuration:Set({parameter_number = 111, size = 4, configuration_value = 300})) -- ...every 5 min @@ -42,7 +17,7 @@ local aeon_meter = { doConfigure = do_configure }, NAME = "aeon meter", - can_handle = can_handle_aeon_meter + can_handle = require("aeon-meter.can_handle"), } return aeon_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua new file mode 100644 index 0000000000..356dbd55df --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_gen5_meter(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-gen5-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeotec-gen5-meter") + end + end + return false +end + +return can_handle_aeotec_gen5_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua new file mode 100644 index 0000000000..3863c27b51 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_GEN5_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0102, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) US + {mfr = 0x0086, prod = 0x0002, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) EU +} + +return AEOTEC_GEN5_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua index 870e104891..1633427372 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/aeotec-gen5-meter/init.lua @@ -1,34 +1,9 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEOTEC_GEN5_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0102, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) US - {mfr = 0x0086, prod = 0x0002, model = 0x005F}, -- Aeotec Home Energy Meter (Gen5) EU -} - -local function can_handle_aeotec_gen5_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_GEN5_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end - local do_configure = function (self, device) device:send(Configuration:Set({parameter_number = 101, size = 4, configuration_value = 3})) -- report total power in Watts and total energy in kWh... device:send(Configuration:Set({parameter_number = 102, size = 4, configuration_value = 0})) -- disable group 2... @@ -43,7 +18,7 @@ local aeotec_gen5_meter = { doConfigure = do_configure }, NAME = "aeotec gen5 meter", - can_handle = can_handle_aeotec_gen5_meter + can_handle = require("aeotec-gen5-meter.can_handle"), } return aeotec_gen5_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/init.lua b/drivers/SmartThings/zwave-electric-meter/src/init.lua index 2ef9f20281..66205758bd 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -31,11 +21,7 @@ local driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("qubino-meter"), - require("aeotec-gen5-meter"), - require("aeon-meter") - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua new file mode 100644 index 0000000000..dd9ae17949 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_meter(opts, driver, device, ...) + local FINGERPRINTS = require("qubino-meter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("qubino-meter") + end + end + return false +end + +return can_handle_qubino_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua new file mode 100644 index 0000000000..5a5fd44be5 --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0007, model = 0x0054}, -- Qubino 3 Phase Meter + {mfr = 0x0159, prod = 0x0007, model = 0x0052} -- Qubino Smart Meter +} + +return QUBINO_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua index a8e2b23805..4c2a40a56e 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/qubino-meter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration @@ -20,22 +10,10 @@ local Meter = (require "st.zwave.CommandClass.Meter")({ version=3 }) --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" -local QUBINO_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0007, model = 0x0054}, -- Qubino 3 Phase Meter - {mfr = 0x0159, prod = 0x0007, model = 0x0052} -- Qubino Smart Meter -} local POWER_UNIT_WATT = "W" local ENERGY_UNIT_KWH = "kWh" -local function can_handle_qubino_meter(opts, driver, device, ...) - for _, fingerprint in ipairs(QUBINO_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function meter_report_handler(self, device, cmd) @@ -101,7 +79,7 @@ local qubino_meter = { init = device_init }, NAME = "qubino meter", - can_handle = can_handle_qubino_meter + can_handle = require("qubino-meter.can_handle"), } return qubino_meter diff --git a/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua b/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua new file mode 100644 index 0000000000..60d4a7380b --- /dev/null +++ b/drivers/SmartThings/zwave-electric-meter/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("qubino-meter"), + lazy_load_if_possible("aeotec-gen5-meter"), + lazy_load_if_possible("aeon-meter"), +} +return sub_drivers 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 d82e1549fa..cb92a861fe 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 09b897ba9d..29255434d1 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 dad4b1030e..55f1b9e436 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 11a1317a74..47391c078f 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 cf640c3655..01c3227147 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" From fb7feeeaa473ca5c53348f21907227f660975630 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 11 Dec 2025 13:21:11 -0800 Subject: [PATCH 336/449] Lifx devices (#2584) * Lifx devices WWSTCERT-8588 WWSTCERT-8609 WWSTCERT-8597 WWSTCERT-8498 WWSTCERT-8594 WWSTCERT-8612 WWSTCERT-8591 WWSTCERT-8519 WWSTCERT-8540 WWSTCERT-8561 WWSTCERT-8486 WWSTCERT-8576 WWSTCERT-8537 WWSTCERT-8516 WWSTCERT-8558 WWSTCERT-8579 WWSTCERT-8573 WWSTCERT-8552 WWSTCERT-8534 WWSTCERT-8504 WWSTCERT-8582 WWSTCERT-8492 WWSTCERT-8606 WWSTCERT-8585 WWSTCERT-8507 WWSTCERT-8495 WWSTCERT-8513 WWSTCERT-8570 WWSTCERT-8489 WWSTCERT-8549 WWSTCERT-8531 WWSTCERT-8510 WWSTCERT-8567 WWSTCERT-8525 WWSTCERT-8528 WWSTCERT-8600 WWSTCERT-8546 WWSTCERT-8543 WWSTCERT-8564 WWSTCERT-8555 * remove duplicate * remove duplicate * remove duplicates * formatting * formatting --- .../matter-switch/fingerprints.yml | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 66564683a8..7d2828a3f4 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -840,6 +840,156 @@ matterManufacturer: vendorId: 0x1423 productId: 0x00BF deviceProfileName: light-color-level + - id: "5155/163" + deviceLabel: LIFX Supercolor (A19) + vendorId: 0x1423 + productId: 0x00A3 + deviceProfileName: light-level-colorTemperature + - id: "5155/118" + deviceLabel: LIFX Lightstrip + vendorId: 0x1423 + productId: 0x0076 + deviceProfileName: light-level-colorTemperature + - id: "5155/221" + deviceLabel: LIFX Spot + vendorId: 0x1423 + productId: 0x00DD + deviceProfileName: light-level-colorTemperature + - id: "5155/144" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x0090 + deviceProfileName: light-level-colorTemperature + - id: "5155/216" + deviceLabel: LIFX Candle Color (B10) + vendorId: 0x1423 + productId: 0x00D8 + deviceProfileName: light-level-colorTemperature + - id: "5155/225" + deviceLabel: LIFX PAR38 + vendorId: 0x1423 + productId: 0x00E1 + deviceProfileName: light-level-colorTemperature + - id: "5155/186" + deviceLabel: LIFX Candle Color + vendorId: 0x1423 + productId: 0x00BA + deviceProfileName: light-level-colorTemperature + - id: "5155/202" + deviceLabel: LIFX Ceiling 13x26 + vendorId: 0x1423 + productId: 0x00CA + deviceProfileName: light-level-colorTemperature + - id: "5155/143" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x008F + deviceProfileName: light-level-colorTemperature + - id: "5155/166" + deviceLabel: LIFX Supercolour (BR30) + vendorId: 0x1423 + productId: 0x00A6 + deviceProfileName: light-level-colorTemperature + - id: "5155/167" + deviceLabel: LIFX Downlight + vendorId: 0x1423 + productId: 0x00A7 + deviceProfileName: light-level-colorTemperature + - id: "5155/207" + deviceLabel: LIFX Everyday Lightstrip + vendorId: 0x1423 + productId: 0x00CF + deviceProfileName: light-level-colorTemperature + - id: "5155/222" + deviceLabel: LIFX Path (Round) + vendorId: 0x1423 + productId: 0x00DE + deviceProfileName: light-level-colorTemperature + - id: "5155/203" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x00CB + deviceProfileName: light-level-colorTemperature + - id: "5155/218" + deviceLabel: LIFX Tube + vendorId: 0x1423 + productId: 0x00DA + deviceProfileName: light-level-colorTemperature + - id: "5155/214" + deviceLabel: LIFX Permanent Outdoor + vendorId: 0x1423 + productId: 0x00D6 + deviceProfileName: light-level-colorTemperature + - id: "5155/117" + deviceLabel: LIFX Lightstrip + vendorId: 0x1423 + productId: 0x0075 + deviceProfileName: light-level-colorTemperature + - id: "5155/223" + deviceLabel: LIFX Downlight (6 Retro Downlight) + vendorId: 0x1423 + productId: 0x00DF + deviceProfileName: light-level-colorTemperature + - id: "5155/224" + deviceLabel: LIFX Downlight (90mm Downlight) + vendorId: 0x1423 + productId: 0x00E0 + deviceProfileName: light-level-colorTemperature + - id: "5155/204" + deviceLabel: LIFX String + vendorId: 0x1423 + productId: 0x00CC + deviceProfileName: light-level-colorTemperature + - id: "5155/206" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x00CE + deviceProfileName: light-level-colorTemperature + - id: "5155/164" + deviceLabel: LIFX Supercolor (BR30) + vendorId: 0x1423 + productId: 0x00A4 + deviceProfileName: light-level-colorTemperature + - id: "5155/120" + deviceLabel: LIFX Beam + vendorId: 0x1423 + productId: 0x0078 + deviceProfileName: light-level-colorTemperature + - id: "5155/208" + deviceLabel: LIFX Everyday Lightstrip + vendorId: 0x1423 + productId: 0x00D0 + deviceProfileName: light-level-colorTemperature + - id: "5155/165" + deviceLabel: LIFX Supercolour (A19) + vendorId: 0x1423 + productId: 0x00A5 + deviceProfileName: light-level-colorTemperature + - id: "5155/142" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x008E + deviceProfileName: light-level-colorTemperature + - id: "5155/141" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x008D + deviceProfileName: light-level-colorTemperature + - id: "5155/177" + deviceLabel: LIFX Ceiling + vendorId: 0x1423 + productId: 0x00B1 + deviceProfileName: light-level-colorTemperature + - id: "5155/170" + deviceLabel: LIFX Supercolour (A21) + vendorId: 0x1423 + productId: 0x00AA + deviceProfileName: light-level-colorTemperature + - id: "5155/205" + deviceLabel: LIFX Neon + vendorId: 0x1423 + productId: 0x00CD + deviceProfileName: light-level-colorTemperature #LG - id: "4142/8784" deviceLabel: LG Smart Button (1 Button) From 8ca4d31fc3ae57af0fbb94316bfcadc9b09f0c28 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 11 Dec 2025 13:21:47 -0800 Subject: [PATCH 337/449] Govee devices (#2631) --- .../matter-switch/fingerprints.yml | 274 +++++++++++++++++- 1 file changed, 272 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 7d2828a3f4..2cd83c4b1b 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -467,7 +467,277 @@ matterManufacturer: vendorId: 0x1339 productId: 0x0016 deviceProfileName: light-color-level-2000K-7000K - +#Govee + - id: "4999/24740" + deviceLabel: Govee Square Ceiling Light (12 inch) + vendorId: 0x1387 + productId: 0x60A4 + deviceProfileName: light-color-level + - id: "4999/28871" + deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 164ft/50m + vendorId: 0x1387 + productId: 0x70C7 + deviceProfileName: light-color-level + - id: "4999/25043" + deviceLabel: Govee Neon Rope Light 2 9.8ft/3m + vendorId: 0x1387 + productId: 0x61D3 + deviceProfileName: light-color-level + - id: "4999/24595" + deviceLabel: Govee RGBWW Smart LED Bulb BR30 850lm + vendorId: 0x1387 + productId: 0x6013 + deviceProfileName: light-color-level + - id: "4999/25011" + deviceLabel: Govee Strip Light with Cover 9.8f/3m + vendorId: 0x1387 + productId: 0x61B3 + deviceProfileName: light-color-level + - id: "4999/24586" + deviceLabel: Govee RGBWW Smart LED Bulb A19 1200lm + vendorId: 0x1387 + productId: 0x600A + deviceProfileName: light-color-level + - id: "4999/26116" + deviceLabel: Govee HDMI 2.1 Sync Box 2 75-85 + vendorId: 0x1387 + productId: 0x6604 + deviceProfileName: light-color-level + - id: "4999/24729" + deviceLabel: Govee TV Backlight 3 Lite 75-85 + vendorId: 0x1387 + productId: 0x6099 + deviceProfileName: light-color-level + - id: "4999/28871" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 164ft/50m + vendorId: 0x1387 + productId: 0x70C7 + deviceProfileName: light-color-level + - id: "4999/28873" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 328ft/100m + vendorId: 0x1387 + productId: 0x70C9 + deviceProfileName: light-color-level + - id: "4999/24754" + deviceLabel: Govee Tree Floor Lamp + vendorId: 0x1387 + productId: 0x60B2 + deviceProfileName: light-color-level + - id: "4999/25078" + deviceLabel: Govee Strip Light 2 Pro 32.8ft/10m + vendorId: 0x1387 + productId: 0x61F6 + deviceProfileName: light-color-level + - id: "4999/25077" + deviceLabel: Govee Strip Light 2 Pro 16.4ft/5m + vendorId: 0x1387 + productId: 0x61F5 + deviceProfileName: light-color-level + - id: "4999/24588" + deviceLabel: Govee RGBWW Smart LED Bulb E14 450lm + vendorId: 0x1387 + productId: 0x600C + deviceProfileName: light-color-level + - id: "4999/28868" + deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 66ft/20m + vendorId: 0x1387 + productId: 0x70C4 + deviceProfileName: light-color-level + - id: "4999/25074" + deviceLabel: Govee Strip Light 2 Pro 6.56ft/2m + vendorId: 0x1387 + productId: 0x61F2 + deviceProfileName: light-color-level + - id: "4999/25013" + deviceLabel: Govee Strip Light with Cover 16.4ft/5m + vendorId: 0x1387 + productId: 0x61B5 + deviceProfileName: light-color-level + - id: "4999/24753" + deviceLabel: Govee Torchiere Floor Lamp + vendorId: 0x1387 + productId: 0x60B1 + deviceProfileName: light-color-level + - id: "4999/28869" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 99ft/30m + vendorId: 0x1387 + productId: 0x70C5 + deviceProfileName: light-color-level + - id: "4999/24592" + deviceLabel: Govee RGBWW Smart LED Bulb BR30 1200lm + vendorId: 0x1387 + productId: 0x6010 + deviceProfileName: light-color-level + - id: "4999/28869" + deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 99ft/30m + vendorId: 0x1387 + productId: 0x70C5 + deviceProfileName: light-color-level + - id: "4999/24607" + deviceLabel: Govee Recessed Downlight 2 (6 inch) + vendorId: 0x1387 + productId: 0x601F + deviceProfileName: light-color-level + - id: "4999/25045" + deviceLabel: Govee Neon Rope Light 2 16.4ft/5m + vendorId: 0x1387 + productId: 0x61D5 + deviceProfileName: light-color-level + - id: "4999/24603" + deviceLabel: Govee Recessed Downlight (4 inch) + vendorId: 0x1387 + productId: 0x601B + deviceProfileName: light-color-level + - id: "4999/24602" + deviceLabel: Govee Recessed Downlight (6 inch) + vendorId: 0x1387 + productId: 0x601A + deviceProfileName: light-color-level + - id: "4999/25014" + deviceLabel: Govee Strip Light with Cover 32.8ft/10m + vendorId: 0x1387 + productId: 0x61B6 + deviceProfileName: light-color-level + - id: "4999/24610" + deviceLabel: Govee Table Lamp 2 + vendorId: 0x1387 + productId: 0x6022 + deviceProfileName: light-color-level + - id: "4999/24606" + deviceLabel: Govee Recessed Downlight 2 (4 inch) + vendorId: 0x1387 + productId: 0x601E + deviceProfileName: light-color-level + - id: "4999/24589" + deviceLabel: Govee RGBWW Smart LED Bulb MR16 400lm + vendorId: 0x1387 + productId: 0x600D + deviceProfileName: light-color-level + - id: "4999/24700" + deviceLabel: Govee Floor Lamp 2 + vendorId: 0x1387 + productId: 0x607C + deviceProfileName: light-color-level + - id: "4999/26116" + deviceLabel: Govee HDMI 2.1 Sync Box 2 55-65 + vendorId: 0x1387 + productId: 0x6604 + deviceProfileName: light-color-level + - id: "4999/24694" + deviceLabel: Govee Floor Lamp Basic + vendorId: 0x1387 + productId: 0x6076 + deviceProfileName: light-color-level + - id: "4999/24587" + deviceLabel: Govee RGBWW Smart LED Bulb E12 450lm + vendorId: 0x1387 + productId: 0x600B + deviceProfileName: light-color-level + - id: "4999/25017" + deviceLabel: Govee Strip Light with Skyline Kit 19.7ft/6m + vendorId: 0x1387 + productId: 0x61B9 + deviceProfileName: light-color-level + - id: "4999/24742" + deviceLabel: Govee Ceiling Light Pro (15 inch) + vendorId: 0x1387 + productId: 0x60A6 + deviceProfileName: light-color-level + - id: "4999/24666" + deviceLabel: Govee TV Backlight 3 Lite Kit 55-65 + vendorId: 0x1387 + productId: 0x605A + deviceProfileName: light-color-level + - id: "4999/24608" + deviceLabel: Govee Table Lamp 2 Pro Sound by JBL + vendorId: 0x1387 + productId: 0x6020 + deviceProfileName: light-color-level + - id: "4999/28868" + deviceLabel: Govee Christmas String Lights 2 (Green Wire) 66ft/20m + vendorId: 0x1387 + productId: 0x70C4 + deviceProfileName: light-color-level + - id: "4999/25046" + deviceLabel: Govee Neon Rope Light 2 32.8ft/10m + vendorId: 0x1387 + productId: 0x61D6 + deviceProfileName: light-color-level + - id: "4999/25061" + deviceLabel: Govee COB Strip Light Pro 9.8ft/3m + vendorId: 0x1387 + productId: 0x61E5 + deviceProfileName: light-color-level + - id: "4999/24723" + deviceLabel: Govee Star Light Projector (Aurora) + vendorId: 0x1387 + productId: 0x6093 + deviceProfileName: light-color-level + - id: "4999/25062" + deviceLabel: Govee COB Strip Light Pro 16.4ft/5m + vendorId: 0x1387 + productId: 0x61E6 + deviceProfileName: light-color-level + - id: "4999/24752" + deviceLabel: Govee Uplighter Floor Lamp + vendorId: 0x1387 + productId: 0x60B0 + deviceProfileName: light-color-level + - id: "4999/24727" + deviceLabel: Govee TV Backlight 3 Lite 40-50 + vendorId: 0x1387 + productId: 0x6097 + deviceProfileName: light-color-level + - id: "4999/24582" + deviceLabel: Govee RGBWW Smart LED Bulb A19 1000lm + vendorId: 0x1387 + productId: 0x6006 + deviceProfileName: light-color-level + - id: "4999/25016" + deviceLabel: Govee Strip Light with Skyline Kit 13.1ft/4m + vendorId: 0x1387 + productId: 0x61B8 + deviceProfileName: light-color-level + - id: "4999/26737" + deviceLabel: Govee Christmas Sparkle String Lights 99ft + vendorId: 0x1387 + productId: 0x6871 + deviceProfileName: light-color-level + - id: "4999/26736" + deviceLabel: Govee Christmas Sparkle String Lights 66ft + vendorId: 0x1387 + productId: 0x6870 + deviceProfileName: light-color-level + - id: "4999/26272" + deviceLabel: Govee TV Backlight 3 Pro 55-65 + vendorId: 0x1387 + productId: 0x66A0 + deviceProfileName: light-color-level + - id: "4999/26272" + deviceLabel: Govee TV Backlight 3 Pro 75-85 + vendorId: 0x1387 + productId: 0x66A0 + deviceProfileName: light-color-level + - id: "4999/28854" + deviceLabel: Govee Curtain Lights Pro + vendorId: 0x1387 + productId: 0x70B6 + deviceProfileName: light-color-level + - id: "4999/24733" + deviceLabel: Govee Galaxy Light Star Projector 2 Pro + vendorId: 0x1387 + productId: 0x609D + deviceProfileName: light-color-level + - id: "4999/24725" + deviceLabel: Govee Star Projector Light + vendorId: 0x1387 + productId: 0x6095 + deviceProfileName: light-color-level + - id: "4999/24724" + deviceLabel: Govee Star Projector Light + vendorId: 0x1387 + productId: 0x6094 + deviceProfileName: light-color-level # Hue - id: "4107/2049" deviceLabel: Hue W 1600 A21 E26 1P NAM @@ -1136,7 +1406,7 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0A9B deviceProfileName: light-level-colorTemperature - - id: 4489/2686 + - id: "4489/2686" deviceLabel: SMART MAT A53 DIM FILGD 824 E27 vendorId: 0x1189 productId: 0x0A7E From 2a14c82c1900bf2bf68780c0b7d3e02eb4677586 Mon Sep 17 00:00:00 2001 From: akangyou <15802427350@163.com> Date: Fri, 12 Dec 2025 06:08:48 +0800 Subject: [PATCH 338/449] add Yanmi fingerprints and cn.csv (#2599) --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 11 +++++++++++ tools/localizations/cn.csv | 2 ++ 2 files changed, 13 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 4c91da651e..3d598372fa 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2400,11 +2400,22 @@ zigbeeManufacturer: manufacturer: NodOn model: SIN-4-1-21 deviceProfileName: switch-power-energy + #Yanmi - id: "JNL/Y-K003-001" deviceLabel: Yanmi Switch (3 Way) 1 manufacturer: JNL model: Y-K003-001 deviceProfileName: basic-switch + - id: "JNL/Y-K001-001" + deviceLabel: Yanmi Switch (1 Way) + manufacturer: JNL + model: Y-K001-001 + deviceProfileName: basic-switch + - id: "JNL/Y-K002-001" + deviceLabel: Yanmi Switch (2 Way) 1 + manufacturer: JNL + model: Y-K002-001 + deviceProfileName: basic-switch zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index e1ea39ff0c..952b4a5c44 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -123,6 +123,8 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "HOPOsmart Window Opener A2230011",HOPOsmart链式开窗器 A2230011 "Yanmi Switch (3 Way)",岩米三位智能开关面板 "Onvis Smart Plug S4EU",Onvis 智能插座S4EU +"Yanmi Switch (2 Way)",岩米二位智能开关面板 +"Yanmi Switch (1 Way)",岩米一位智能开关面板 "WISTAR WSCMQ Smart Curtain Motor",威仕达智能开合帘电机 WSCMQ "WISTAR WSCMXI Smart Curtain Motor",威仕达智能开合帘电机 WSCMXI "WISTAR WSCMT Smart Curtain Motor",威仕达智能开合帘电机 WSCMT From 873a288be15989fd44189e6414189f76a753d1cb Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:32 -0600 Subject: [PATCH 339/449] CHAD-17094: zwave-valve lazy loading of sub-drivers --- drivers/SmartThings/zwave-valve/src/init.lua | 21 ++++--------------- .../src/inverse_valve/can_handle.lua | 11 ++++++++++ .../zwave-valve/src/inverse_valve/init.lua | 20 ++++-------------- .../zwave-valve/src/lazy_load_subdriver.lua | 18 ++++++++++++++++ .../zwave-valve/src/sub_drivers.lua | 8 +++++++ .../src/test/test_inverse.valve.lua | 16 +++----------- .../zwave-valve/src/test/test_zwave_valve.lua | 16 +++----------- 7 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua create mode 100644 drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-valve/src/sub_drivers.lua diff --git a/drivers/SmartThings/zwave-valve/src/init.lua b/drivers/SmartThings/zwave-valve/src/init.lua index 43de12dcc4..cea5b877c1 100644 --- a/drivers/SmartThings/zwave-valve/src/init.lua +++ b/drivers/SmartThings/zwave-valve/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -26,10 +16,7 @@ local driver_template = { supported_capabilities = { capabilities.valve, }, - sub_drivers = { - -- Fortrezz and Zooz valves treat open as "off" and close as "on" - require("inverse_valve") - }, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua b/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua new file mode 100644 index 0000000000..b36334f2c9 --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/inverse_valve/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function inverse_valve_can_handle(opts, driver, device, ...) + if device.zwave_manufacturer_id == 0x0084 or device.zwave_manufacturer_id == 0x027A then + return true, require("inverse_valve") + end + return false +end + +return inverse_valve_can_handle diff --git a/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua b/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua index 510e79019b..853bada351 100644 --- a/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua +++ b/drivers/SmartThings/zwave-valve/src/inverse_valve/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -55,9 +45,7 @@ local inverse_valve = { [capabilities.valve.commands.close.NAME] = close_handler } }, - can_handle = function(opts, driver, device, ...) - return device.zwave_manufacturer_id == 0x0084 or device.zwave_manufacturer_id == 0x027A - end + can_handle = require("inverse_valve.can_handle"), } return inverse_valve diff --git a/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-valve/src/sub_drivers.lua b/drivers/SmartThings/zwave-valve/src/sub_drivers.lua new file mode 100644 index 0000000000..b43856e0cd --- /dev/null +++ b/drivers/SmartThings/zwave-valve/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("inverse_valve"), +} +return sub_drivers 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 db7d6be656..68c563061b 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 4a21568119..efa0c42599 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" From 416fdd3556153c0e60e00c66f6e1235b6a9e1d15 Mon Sep 17 00:00:00 2001 From: seojune Date: Fri, 12 Dec 2025 10:58:30 +0900 Subject: [PATCH 340/449] Correcting Incorrect Battery Information --- .../zigbee-button/src/aqara/init.lua | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index baa03c4f34..1fb762cbbc 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -34,13 +34,13 @@ local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 local PRESENT_ATTRIBUTE_ID = 0x0055 local COMP_LIST = { "button1", "button2", "all" } -local FINGERPRINTS = { - ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3 }, - ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1 }, - ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3 } +local AQARA_REMOTE_SWITCH = { + ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Mini Switch T1 + ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch E1 (Single Rocker) + ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Single Rocker) + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Double Rocker) + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2450", quantity = 1 }, -- Aqara Wireless Remote Switch H1 (Single Rocker) + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2450", quantity = 1 } -- Aqara Wireless Remote Switch H1 (Double Rocker) } local configuration = { @@ -65,7 +65,7 @@ local configuration = { local function present_value_attr_handler(driver, device, value, zb_rx) if value.value < 0xFF then local end_point = zb_rx.address_header.src_endpoint.value - local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 local evt = capabilities.button.button.held({ state_change = true }) if value.value == 1 then evt = capabilities.button.button.pushed({ state_change = true }) @@ -110,7 +110,7 @@ local function battery_level_handler(driver, device, value, zb_rx) end local function mode_switching_handler(driver, device, value, zb_rx) - local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 local allow = device.preferences[MODE_CHANGE] or false if allow then local mode = device:get_field(MODE) or 1 @@ -141,7 +141,7 @@ end local is_aqara_products = function(opts, driver, device) local isAqaraProducts = false - if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then + if AQARA_REMOTE_SWITCH[device:get_model()] and AQARA_REMOTE_SWITCH[device:get_model()].mfr == device:get_manufacturer() then isAqaraProducts = true end return isAqaraProducts @@ -157,9 +157,11 @@ local function device_init(driver, device) end local function added_handler(self, device) - local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 local mode = device:get_field(MODE) or 0 local model = device:get_model() + local type = AQARA_REMOTE_SWITCH[device:get_model()].type or "CR2032" + local quantity = AQARA_REMOTE_SWITCH[device:get_model()].quantity or 1 if mode == 0 then if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then @@ -175,8 +177,8 @@ local function added_handler(self, device) button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) device:emit_event(capabilities.batteryLevel.battery.normal()) - device:emit_event(capabilities.batteryLevel.type("CR2032")) - device:emit_event(capabilities.batteryLevel.quantity(1)) + device:emit_event(capabilities.batteryLevel.type(type)) + device:emit_event(capabilities.batteryLevel.quantity(quantity)) if btn_evt_cnt > 1 then for i = 1, btn_evt_cnt do From 1c8e9319882cfa75a60be1c7f9aecff5ede5d86c Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 12 Dec 2025 13:17:04 -0600 Subject: [PATCH 341/449] add InitialPress to Ikea Scroll subscription --- .../src/sub_drivers/ikea_scroll/scroll_utils/fields.lua | 4 ++-- .../SmartThings/matter-switch/src/test/test_ikea_scroll.lua | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) 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 3e676b2912..fff0a1cce4 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 @@ -11,9 +11,9 @@ IkeaScrollFields.ENDPOINT_POWER_SOURCE = 0 -- Switch Endpoints used for basic press functionality IkeaScrollFields.ENDPOINTS_PRESS = {3, 6, 9} --- Required Events for the ENDPOINTS_PRESS. Remove the default subscription to --- InitialPress since this is a MultiPress device and InitialPress will be ignored. +-- Required Events for the ENDPOINTS_PRESS. IkeaScrollFields.switch_press_subscribed_events = { + clusters.Switch.events.InitialPress.ID, clusters.Switch.events.MultiPressComplete.ID, clusters.Switch.events.LongPress.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 f56b1e7738..c560386e37 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -127,6 +127,7 @@ local ENDPOINTS_PRESS = { 3, 6, 9 } -- the ikea scroll subdriver has overriden subscribe behavior local function ikea_scroll_subscribe() local CLUSTER_SUBSCRIBE_LIST ={ + clusters.Switch.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.MultiPressComplete, } From 7d48d6c094aab90868ea1e16ff175e37a05b663d Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:10 -0600 Subject: [PATCH 342/449] CHAD-17082: zigbee-water-leak-sensor lazy loading of sub-drivers --- .../src/aqara/can_handle.lua | 14 ++++++++ .../src/aqara/fingerprints.lua | 8 +++++ .../src/aqara/init.lua | 16 +++------- .../src/configurations.lua | 16 ++-------- .../src/frient/can_handle.lua | 11 +++++++ .../src/frient/init.lua | 20 +++--------- .../zigbee-water-leak-sensor/src/init.lua | 26 +++------------ .../src/lazy_load_subdriver.lua | 15 +++++++++ .../src/leaksmart/can_handle.lua | 11 +++++++ .../src/leaksmart/init.lua | 20 +++--------- .../src/sengled/can_handle.lua | 14 ++++++++ .../src/sengled/fingerprints.lua | 8 +++++ .../src/sengled/init.lua | 16 +++------- .../src/sinope/can_handle.lua | 13 ++++++++ .../src/sinope/init.lua | 29 +++-------------- .../src/sub_drivers.lua | 14 ++++++++ .../src/test/test_aqara_water_leak_sensor.lua | 16 ++-------- .../test_centralite_water_leak_sensor.lua | 16 ++-------- .../test/test_frient_water_leak_sensor.lua | 16 ++-------- .../src/test/test_leaksmart_water.lua | 16 ++-------- .../test/test_samjin_water_leak_sensor.lua | 16 ++-------- .../test/test_sengled_water_leak_sensor.lua | 16 ++-------- .../src/test/test_sinope_zigbee_water.lua | 16 ++-------- .../test_smartthings_water_leak_sensor.lua | 16 ++-------- .../test_thirdreality_water_leak_sensor.lua | 16 ++-------- .../src/test/test_zigbee_water.lua | 16 ++-------- .../src/test/test_zigbee_water_freeze.lua | 16 ++-------- .../src/thirdreality/can_handle.lua | 14 ++++++++ .../src/thirdreality/fingerprints.lua | 9 ++++++ .../src/thirdreality/init.lua | 32 +++---------------- .../src/zigbee-water-freeze/can_handle.lua | 11 +++++++ .../src/zigbee-water-freeze/init.lua | 20 +++--------- 32 files changed, 212 insertions(+), 301 deletions(-) create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..f0ba630b6f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.flood.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua index 7319bae636..595a10f64e 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -10,9 +13,6 @@ local MFG_CODE = 0x115F local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.flood.agl02" } -} local CONFIGURATIONS = { { @@ -25,14 +25,6 @@ local CONFIGURATIONS = { } } -local function is_aqara_products(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(driver, device) device:emit_event(capabilities.waterSensor.water.dry()) @@ -56,7 +48,7 @@ local aqara_contact_handler = { init = device_init, added = device_added }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_contact_handler diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua index a5086d979e..92b7ef226b 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local PowerConfiguration = clusters.PowerConfiguration diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua new file mode 100644 index 0000000000..e7c5dd9c23 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and device:get_model() == "FLSZB-110" then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua index 5e92ef80d6..4357eb91b2 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local TemperatureMeasurement = clusters.TemperatureMeasurement @@ -43,9 +33,7 @@ local frient_water_leak_sensor = { init = device_init, doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and device:get_model() == "FLSZB-110" - end + can_handle = require("frient.can_handle"), } return frient_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua index 68a218a586..4ded4e195c 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -89,15 +79,7 @@ local zigbee_water_driver_template = { added = added_handler }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, - sub_drivers = { - require("aqara"), - require("zigbee-water-freeze"), - require("leaksmart"), - require("frient"), - require("thirdreality"), - require("sengled"), - require("sinope") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua new file mode 100644 index 0000000000..67c6f425ca --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function leaksmart_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "WAXMAN" and device:get_model() == "leakSMART Water Sensor V2" then + return true, require("leaksmart") + end + return false +end + +return leaksmart_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua index 010379df0f..a8ab5212b5 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/leaksmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -58,9 +48,7 @@ local leaksmart_water_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "WAXMAN" and device:get_model() == "leakSMART Water Sensor V2" - end + can_handle = require("leaksmart.can_handle"), } return leaksmart_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..2c3f527da6 --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E1L-G7K" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua index f00263e309..0010073033 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E1L-G7K" } -} local CONFIGURATIONS = { { @@ -27,14 +27,6 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(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_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) @@ -49,7 +41,7 @@ local sengled_water_leak_sensor_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_water_leak_sensor_handler diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua new file mode 100644 index 0000000000..757e3ca0db --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sinope_water_sensor = function(opts, driver, device) + local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" + if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then + return true, require("sinope") + else + return false + end +end + +return is_sinope_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua index 65cc802697..7bf6f8be82 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sinope/init.lua @@ -1,23 +1,11 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local IASZone = zcl_clusters.IASZone -local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" - local generate_event_from_zone_status = function(driver, device, zone_status, zb_rx) local event if zone_status:is_alarm1_set() then @@ -39,13 +27,6 @@ local ias_zone_status_change_handler = function(driver, device, zb_rx) generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) end -local is_sinope_water_sensor = function(opts, driver, device) - if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then - return true - else - return false - end -end local sinope_water_sensor = { NAME = "Sinope Water Leak Sensor", @@ -61,7 +42,7 @@ local sinope_water_sensor = { } } }, - can_handle = is_sinope_water_sensor + can_handle = require("sinope.can_handle"), } -return sinope_water_sensor \ No newline at end of file +return sinope_water_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..79d572579e --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- 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("aqara"), + lazy_load_if_possible("zigbee-water-freeze"), + lazy_load_if_possible("leaksmart"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("sengled"), + lazy_load_if_possible("sinope"), +} +return sub_drivers 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 495a8455b3..ea80228581 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 4ee674199c..b42f3484ae 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 13bdb14268..56c264a505 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 7b20d7c3bb..86e4aa774a 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 43543e3127..3eb7cb201a 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d4ad7bccff..348ab81f0e 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 bc476f06bb..19e1c7053e 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 e4ffe5567c..2b9ee2d3c1 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d32f94e255..b52884f6a3 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 ab0163ef5c..3221e4fc11 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 3186af8245..9188bbc996 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..044050b14f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_third_reality_water_leak_sensor(opts, driver, device) + local FINGERPRINTS = require("thirdreality.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("thirdreality") + end + end + return false +end + +return can_handle_third_reality_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua new file mode 100644 index 0000000000..f56a027f4f --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS = { + { mfr = "Third Reality, Inc", model = "3RWS18BZ"}, + { mfr = "THIRDREALITY", model = "3RWS18BZ"} +} + +return THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua index 99f14294e9..8a3819ac44 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/thirdreality/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" local utils = require "st.utils" @@ -20,19 +10,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local APPLICATION_VERSION = "application_version" -local THIRD_REALITY_WATER_LEAK_SENSOR_FINGERPRINTS = { - { mfr = "Third Reality, Inc", model = "3RWS18BZ"}, - { mfr = "THIRDREALITY", model = "3RWS18BZ"} -} -local function can_handle_third_reality_water_leak_sensor(opts, driver, device) - for _, fingerprint in ipairs(THIRD_REALITY_WATER_LEAK_SENSOR_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(driver, device) device:set_field(APPLICATION_VERSION, 0) @@ -73,7 +51,7 @@ local third_reality_water_leak_sensor = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_third_reality_water_leak_sensor + can_handle = require("thirdreality.can_handle"), } -return third_reality_water_leak_sensor \ No newline at end of file +return third_reality_water_leak_sensor diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua new file mode 100644 index 0000000000..5850a8b11c --- /dev/null +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function zigbee_water_freeze_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Ecolink" and device:get_model() == "FLZB1-ECO" then + return true, require("zigbee-water-freeze") + end + return false +end + +return zigbee_water_freeze_can_handle diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua index 68bd2ba9e0..82aca1b710 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/zigbee-water-freeze/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -68,9 +58,7 @@ local zigbee_water_freeze = { init = battery_defaults.build_linear_voltage_init(2.2, 3.0), doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Ecolink" and device:get_model() == "FLZB1-ECO" - end + can_handle = require("zigbee-water-freeze.can_handle"), } return zigbee_water_freeze From 37d3453784d695b9d51e846aec29724a0dedfb23 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:39 -0600 Subject: [PATCH 343/449] CHAD-17093: zwave-siren lazy loading of sub-drivers --- .../zwave-siren/src/aeon-siren/can_handle.lua | 14 ++++++++ .../zwave-siren/src/aeon-siren/init.lua | 24 +++---------- .../src/aeotec-doorbell-siren/can_handle.lua | 14 ++++++++ .../aeotec-doorbell-siren/fingerprints.lua | 13 +++++++ .../src/aeotec-doorbell-siren/init.lua | 34 +++---------------- .../src/apiv6_bugfix/can_handle.lua | 17 ++++++++++ .../zwave-siren/src/apiv6_bugfix/init.lua | 11 +++--- .../zwave-siren/src/configurations.lua | 16 ++------- .../src/ecolink-wireless-siren/can_handle.lua | 14 ++++++++ .../ecolink-wireless-siren/fingerprints.lua | 8 +++++ .../src/ecolink-wireless-siren/init.lua | 29 +++------------- .../zwave-siren/src/fortrezz/can_handle.lua | 13 +++++++ .../zwave-siren/src/fortrezz/init.lua | 25 +++----------- drivers/SmartThings/zwave-siren/src/init.lua | 30 +++------------- .../zwave-siren/src/lazy_load_subdriver.lua | 18 ++++++++++ .../src/multifunctional-siren/can_handle.lua | 14 ++++++++ .../multifunctional-siren/fingerprints.lua | 9 +++++ .../src/multifunctional-siren/init.lua | 30 +++------------- .../src/philio-sound-siren/can_handle.lua | 14 ++++++++ .../src/philio-sound-siren/fingerprints.lua | 8 +++++ .../src/philio-sound-siren/init.lua | 29 +++------------- .../zwave-siren/src/preferences.lua | 16 ++------- .../zwave-siren/src/sub_drivers.lua | 18 ++++++++++ .../zwave-siren/src/test/test_aeon_siren.lua | 16 ++------- .../src/test/test_aeotec_doorbell_siren.lua | 16 ++------- .../src/test/test_ecolink_wireless_siren.lua | 16 ++------- .../src/test/test_fortrezz_siren.lua | 18 +++------- .../src/test/test_philio_sound_siren.lua | 16 ++------- .../src/test/test_utilitech_siren.lua | 16 ++------- .../zwave-siren/src/test/test_yale_siren.lua | 16 ++------- .../src/test/test_zipato_siren.lua | 16 ++------- .../test/test_zwave_multifunctional-siren.lua | 16 ++------- .../test/test_zwave_notification_siren.lua | 16 ++------- .../zwave-siren/src/test/test_zwave_siren.lua | 2 +- .../src/test/test_zwave_sound_sensor.lua | 16 ++------- .../src/utilitech-siren/can_handle.lua | 13 +++++++ .../zwave-siren/src/utilitech-siren/init.lua | 25 +++----------- .../zwave-siren/src/yale-siren/can_handle.lua | 12 +++++++ .../zwave-siren/src/yale-siren/init.lua | 24 +++---------- .../src/zipato-siren/can_handle.lua | 12 +++++++ .../zwave-siren/src/zipato-siren/init.lua | 22 +++--------- .../src/zwave-sound-sensor/can_handle.lua | 14 ++++++++ .../src/zwave-sound-sensor/fingerprints.lua | 8 +++++ .../src/zwave-sound-sensor/init.lua | 29 +++------------- 44 files changed, 323 insertions(+), 434 deletions(-) create mode 100644 drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-siren/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua diff --git a/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua new file mode 100644 index 0000000000..2aae744ccc --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeon-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeon_siren(opts, driver, device, ...) + local AEON_MFR = 0x0086 + local AEON_SIREN_PRODUCT_ID = 0x0050 + + if device.zwave_manufacturer_id == AEON_MFR and device.zwave_product_id == AEON_SIREN_PRODUCT_ID then + return true, require("aeon-siren") + end + return false +end + +return can_handle_aeon_siren diff --git a/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua b/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua index f98ba06d83..1c1a2bb0b1 100644 --- a/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/aeon-siren/init.lua @@ -1,32 +1,16 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=1 }) -local AEON_MFR = 0x0086 -local AEON_SIREN_PRODUCT_ID = 0x0050 - local SOUND_TYPE_AND_VOLUME_PARAMETER_NUMBER = 37 local CONFIGURE_SOUND_TYPE = "type" local SOUND_TYPE_DEFAULT = 1 local CONFIGURE_VOLUME = "volume" local VOLUME_DEFAULT = 3 -local function can_handle_aeon_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == AEON_MFR and device.zwave_product_id == AEON_SIREN_PRODUCT_ID -end local function configure_sound(device, sound_type, volume) if sound_type == nil then sound_type = SOUND_TYPE_DEFAULT end @@ -64,7 +48,7 @@ end local aeon_siren = { NAME = "aeon-siren", - can_handle = can_handle_aeon_siren, + can_handle = require("aeon-siren.can_handle"), lifecycle_handlers = { doConfigure = do_configure, infoChanged = info_changed diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua new file mode 100644 index 0000000000..31304e8688 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_doorbell_siren(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-doorbell-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("aeotec-doorbell-siren") + end + end + return false +end + +return can_handle_aeotec_doorbell_siren diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua new file mode 100644 index 0000000000..782dfbc0c9 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_DOORBELL_SIREN_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A2}, -- Aeotec Doorbell 6 (EU) + { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A2}, -- Aeotec Doorbell 6 (US) + { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A2}, -- Aeotec Doorbell 6 (AU) + { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A4}, -- Aeotec Siren 6 (EU) + { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A4}, -- Aeotec Siren 6 (US) + { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A4}, -- Aeotec Siren 6 (AU) +} + +return AEOTEC_DOORBELL_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua index bab6909235..35f006e94f 100644 --- a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -20,14 +10,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local SoundSwitch = (require "st.zwave.CommandClass.SoundSwitch")({version=1}) local preferencesMap = require "preferences" -local AEOTEC_DOORBELL_SIREN_FINGERPRINTS = { - { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A2}, -- Aeotec Doorbell 6 (EU) - { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A2}, -- Aeotec Doorbell 6 (US) - { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A2}, -- Aeotec Doorbell 6 (AU) - { manufacturerId = 0x0371, productType = 0x0003, productId = 0x00A4}, -- Aeotec Siren 6 (EU) - { manufacturerId = 0x0371, productType = 0x0103, productId = 0x00A4}, -- Aeotec Siren 6 (US) - { manufacturerId = 0x0371, productType = 0x0203, productId = 0x00A4}, -- Aeotec Siren 6 (AU) -} local COMPONENT_NAME = "componentName" local TONE = "tone" @@ -51,14 +33,6 @@ local BUTTON_BATTERY_NORMAL = 99 local DEVICE_PROFILE_CHANGE_IN_PROGRESS = "device_profile_change_in_progress" local NEXT_BUTTON_BATTERY_EVENT_DETAILS = "next_button_battery_event_details" -local function can_handle_aeotec_doorbell_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_DOORBELL_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function querySoundStatus(device) for endpoint = 2, NUMBER_OF_SOUND_COMPONENTS do @@ -316,7 +290,7 @@ end local aeotec_doorbell_siren = { NAME = "aeotec-doorbell-siren", - can_handle = can_handle_aeotec_doorbell_siren, + can_handle = require("aeotec-doorbell-siren.can_handle"), lifecycle_handlers = { added = device_added, diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..3f4b44c1e0 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION + then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua index 0204b7b2d5..2e7e3ca3b8 100644 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua @@ -1,13 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end local function wakeup_notification(driver, device, cmd) device:refresh() @@ -20,7 +17,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-siren/src/configurations.lua b/drivers/SmartThings/zwave-siren/src/configurations.lua index b17b3cbc0f..5165ee4551 100644 --- a/drivers/SmartThings/zwave-siren/src/configurations.lua +++ b/drivers/SmartThings/zwave-siren/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { YALE_SIREN = { diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua new file mode 100644 index 0000000000..5803087913 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_wireless_siren(opts, driver, device, ...) + local FINGERPRINTS = require("ecolink-wireless-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("ecolink-wireless-siren") + end + end + return false +end + +return can_handle_ecolink_wireless_siren diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua new file mode 100644 index 0000000000..9edaef0374 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ECOLINK_WIRELESS_SIREN_FINGERPRINTS = { + { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000A }, -- Ecolink Siren +} + +return ECOLINK_WIRELESS_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua index 0413221185..6b5a2a25ad 100644 --- a/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/ecolink-wireless-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,18 +10,7 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) --- @type st.zwave.CommandClass.SwitchBinary local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) -local ECOLINK_WIRELESS_SIREN_FINGERPRINTS = { - { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000A }, -- Ecolink Siren -} -local function can_handle_ecolink_wireless_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_WIRELESS_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function basic_set_handler(driver, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value @@ -103,7 +82,7 @@ local ecolink_wireless_siren = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_ecolink_wireless_siren, + can_handle = require("ecolink-wireless-siren.can_handle"), } return ecolink_wireless_siren diff --git a/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua b/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua new file mode 100644 index 0000000000..83289ab86a --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/fortrezz/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fortrezz_siren(opts, self, device, ...) + if device.zwave_manufacturer_id == 0x0084 and + device.zwave_product_type == 0x0313 and + device.zwave_product_id == 0x010B then + return true, require("fortrezz") + end + return false +end + +return can_handle_fortrezz_siren diff --git a/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua b/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua index 73a3e6e0d4..8bf31178f6 100644 --- a/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua +++ b/drivers/SmartThings/zwave-siren/src/fortrezz/init.lua @@ -1,26 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) -local function can_handle_fortrezz_siren(opts, self, device, ...) - return device.zwave_manufacturer_id == 0x0084 and - device.zwave_product_type == 0x0313 and - device.zwave_product_id == 0x010B -end local function set_and_get(value) return function (self, device, command) @@ -47,7 +32,7 @@ end local fortrezz_siren = { NAME = "fortrezz-siren", - can_handle = can_handle_fortrezz_siren, + can_handle = require("fortrezz.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.siren.NAME] = set_and_get(0x42), @@ -67,4 +52,4 @@ local fortrezz_siren = { } } -return fortrezz_siren \ No newline at end of file +return fortrezz_siren diff --git a/drivers/SmartThings/zwave-siren/src/init.lua b/drivers/SmartThings/zwave-siren/src/init.lua index a725e8b79c..b682ea77b7 100644 --- a/drivers/SmartThings/zwave-siren/src/init.lua +++ b/drivers/SmartThings/zwave-siren/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cap_defaults = require "st.capabilities.defaults" @@ -92,19 +82,7 @@ local driver_template = { capabilities.relativeHumidityMeasurement, capabilities.chime }, - sub_drivers = { - require("multifunctional-siren"), - require("zwave-sound-sensor"), - require("ecolink-wireless-siren"), - require("philio-sound-siren"), - require("aeotec-doorbell-siren"), - require("aeon-siren"), - require("yale-siren"), - require("zipato-siren"), - require("utilitech-siren"), - require("fortrezz"), - require("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { infoChanged = info_changed, doConfigure = do_configure, diff --git a/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua new file mode 100644 index 0000000000..a3c62d02aa --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multifunctional_siren(opts, driver, device, ...) + local FINGERPRINTS = require("multifunctional-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("multifunctional-siren") + end + end + return false +end + +return can_handle_multifunctional_siren diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua new file mode 100644 index 0000000000..d2bcf5402d --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MULTIFUNCTIONAL_SIREN_FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x000C, productId = 0x0003 }, -- Zooz S2 Multisiren ZSE19 + { manufacturerId = 0x0060, productType = 0x000C, productId = 0x0003 } -- Everspring Indoor Voice Siren +} + +return MULTIFUNCTIONAL_SIREN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua index 2c8af3bb26..b1bca996d5 100644 --- a/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/multifunctional-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,24 +10,12 @@ local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) --- @type st.zwave.CommandClass.Battery local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local MULTIFUNCTIONAL_SIREN_FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x000C, productId = 0x0003 }, -- Zooz S2 Multisiren ZSE19 - { manufacturerId = 0x0060, productType = 0x000C, productId = 0x0003 } -- Everspring Indoor Voice Siren -} --- Determine whether the passed device is multifunctional siren --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_multifunctional_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTIFUNCTIONAL_SIREN_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end --- Default handler for notification command class reports --- @@ -74,7 +52,7 @@ local multifunctional_siren = { doConfigure = do_configure }, NAME = "multifunctional siren", - can_handle = can_handle_multifunctional_siren, + can_handle = require("multifunctional-siren.can_handle"), } return multifunctional_siren diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua new file mode 100644 index 0000000000..23e614a2e2 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_philio_sound_siren(opts, driver, device, ...) + local FINGERPRINTS = require("philio-sound-siren.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("philio-sound-siren") + end + end + return false +end + +return can_handle_philio_sound_siren diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua new file mode 100644 index 0000000000..e1f34617fc --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local PHILIO_SOUND_SIREN = { + { manufacturerId = 0x013C, productType = 0x0004, productId = 0x000A } +} + +return PHILIO_SOUND_SIREN diff --git a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua index 6397022dd8..23e310bf14 100644 --- a/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/philio-sound-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -19,9 +9,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version=2}) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local preferencesMap = require "preferences" -local PHILIO_SOUND_SIREN = { - { manufacturerId = 0x013C, productType = 0x0004, productId = 0x000A } -} local PARAMETER_SOUND = "sound" local SMOKE = 0 @@ -43,14 +30,6 @@ local sounds = { [SMOKE] = {notificationType = Notification.notification_type.SMOKE, event = Notification.event.smoke.DETECTED_LOCATION_PROVIDED} } -local function can_handle_philio_sound_siren(opts, driver, device, ...) - for _, fingerprint in ipairs(PHILIO_SOUND_SIREN) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function device_added(self, device) device:refresh() @@ -157,7 +136,7 @@ end local philio_sound_siren = { NAME = "Philio sound siren", - can_handle = can_handle_philio_sound_siren, + can_handle = require("philio-sound-siren.can_handle"), lifecycle_handlers = { added = device_added }, diff --git a/drivers/SmartThings/zwave-siren/src/preferences.lua b/drivers/SmartThings/zwave-siren/src/preferences.lua index 3e9f8333ab..2c10de6fcb 100644 --- a/drivers/SmartThings/zwave-siren/src/preferences.lua +++ b/drivers/SmartThings/zwave-siren/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { PHILIO_SOUND_SIREN = { diff --git a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua new file mode 100644 index 0000000000..a20d559e44 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua @@ -0,0 +1,18 @@ +-- 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("multifunctional-siren"), + lazy_load_if_possible("zwave-sound-sensor"), + lazy_load_if_possible("ecolink-wireless-siren"), + lazy_load_if_possible("philio-sound-siren"), + lazy_load_if_possible("aeotec-doorbell-siren"), + lazy_load_if_possible("aeon-siren"), + lazy_load_if_possible("yale-siren"), + lazy_load_if_possible("zipato-siren"), + lazy_load_if_possible("utilitech-siren"), + lazy_load_if_possible("fortrezz"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers 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 714dc17a10..34cdc4ceea 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 c2dc708cfe..924ef4e4f2 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" 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 f20e5c6074..54b4243331 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 e23b6d3caf..e99cfce77c 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" @@ -163,4 +153,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 23ad20a1fd..d825be6cec 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" 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 4c24c069ad..e62141ebcf 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 072a55c3d3..a7b7123f38 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 fbfbdd2db7..6b912be8bb 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 c0d58bb952..a40b510b49 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 73de507946..0e0f407523 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 75713b89b7..121f6170e2 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua @@ -1,4 +1,4 @@ ----@diagnostic disable: param-type-mismatch, undefined-field +-- Copyright 2022 SmartThings, Inc. -- Copyright 2022 SmartThings -- -- Licensed under the Apache License, Version 2.0 (the "License"); 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 e88b1e1852..10595c7137 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua new file mode 100644 index 0000000000..72f6a963b9 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/utilitech-siren/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_utilitech_siren(opts, driver, device, ...) + local UTILITECH_MFR = 0x0060 + local UTILITECH_SIREN_PRODUCT_ID = 0x0001 + if device.zwave_manufacturer_id == UTILITECH_MFR and device.zwave_product_id == UTILITECH_SIREN_PRODUCT_ID then + return true, require("utilitech-siren") + end + return false +end + +return can_handle_utilitech_siren diff --git a/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua b/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua index 9e6818c6e4..0088fa6dd8 100644 --- a/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/utilitech-siren/init.lua @@ -1,29 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({version=1,strict=true}) local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local BatteryDefaults = require "st.zwave.defaults.battery" -local UTILITECH_MFR = 0x0060 -local UTILITECH_SIREN_PRODUCT_ID = 0x0001 - -local function can_handle_utilitech_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == UTILITECH_MFR and device.zwave_product_id == UTILITECH_SIREN_PRODUCT_ID -end - local function device_added(self, device) device:send(Basic:Get({})) device:send(Battery:Get({})) @@ -39,7 +22,7 @@ end local utilitech_siren = { NAME = "utilitech-siren", - can_handle = can_handle_utilitech_siren, + can_handle = require("utilitech-siren.can_handle"), zwave_handlers = { [cc.BATTERY] = { [Battery.REPORT] = battery_report_handler diff --git a/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua new file mode 100644 index 0000000000..1e29867936 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/yale-siren/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_yale_siren(opts, self, device, ...) + local YALE_MFR = 0x0129 + if device.zwave_manufacturer_id == YALE_MFR then + return true, require("yale-siren") + end + return false +end + +return can_handle_yale_siren diff --git a/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua b/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua index de548aee95..3757a2c3a9 100644 --- a/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/yale-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -20,12 +10,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1 local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=1}) local preferencesMap = require "preferences" -local YALE_MFR = 0x0129 - -local function can_handle_yale_siren(opts, self, device, ...) - return device.zwave_manufacturer_id == YALE_MFR -end - local function siren_set_helper(device, value) device:send(Basic:Set({value = value})) local query_device = function() @@ -98,7 +82,7 @@ end local yale_siren = { NAME = "yale-siren", - can_handle = can_handle_yale_siren, + can_handle = require("yale-siren.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.both.NAME] = siren_on, diff --git a/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua b/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua new file mode 100644 index 0000000000..cb1170db2e --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zipato-siren/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zipato_siren(opts, driver, device, ...) + local ZIPATO_MFR = 0x0131 + if device.zwave_manufacturer_id == ZIPATO_MFR then + return true, require("zipato-siren") + end + return false +end + +return can_handle_zipato_siren diff --git a/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua b/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua index e22e643ae1..2af57daa19 100644 --- a/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/zipato-siren/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -18,13 +8,9 @@ local AlarmDefaults = require "st.zwave.defaults.alarm" local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) -local ZIPATO_MFR = 0x0131 local BASIC_AND_SWITCH_BINARY_REPORT_STROBE_LIMIT = 33 local BASIC_AND_SWITCH_BINARY_REPORT_SIREN_LIMIT = 66 -local function can_handle_zipato_siren(opts, driver, device, ...) - return device.zwave_manufacturer_id == ZIPATO_MFR -end local function basic_report_handler(driver, device, cmd) local value = cmd.args.value @@ -64,7 +50,7 @@ end local zipato_siren = { NAME = "zipato-siren", - can_handle = can_handle_zipato_siren, + can_handle = require("zipato-siren.can_handle"), capability_handlers = { [capabilities.alarm.ID] = { [capabilities.alarm.commands.both.NAME] = siren_on, diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua new file mode 100644 index 0000000000..c3fb8b343e --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zwave_sound_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-sound-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-sound-sensor") + end + end + return false +end + +return can_handle_zwave_sound_sensor diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua new file mode 100644 index 0000000000..e0c8ef4762 --- /dev/null +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_SOUND_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000F } --Ecolink Firefighter +} + +return ZWAVE_SOUND_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua index c591f8a89b..c50153ec26 100644 --- a/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua +++ b/drivers/SmartThings/zwave-siren/src/zwave-sound-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +8,12 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) -local ZWAVE_SOUND_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x014A, productType = 0x0005, productId = 0x000F } --Ecolink Firefighter -} --- Determine whether the passed device is zwave-sound-sensor --- --- @param driver Driver driver instance --- @param device Device device isntance --- @return boolean true if the device proper, else false -local function can_handle_zwave_sound_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_SOUND_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end --- Default handler for alarm command class reports --- @@ -73,7 +52,7 @@ local zwave_sound_sensor = { added = added_handler, }, NAME = "zwave sound sensor", - can_handle = can_handle_zwave_sound_sensor, + can_handle = require("zwave-sound-sensor.can_handle"), } return zwave_sound_sensor From f9b30eea8456f0c324efb2cde1864b8c102e8ae2 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 12 Dec 2025 14:10:21 -0800 Subject: [PATCH 344/449] comment out govee duplicate fingerprints --- .../matter-switch/fingerprints.yml | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 2cd83c4b1b..a53fc144a6 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -508,11 +508,11 @@ matterManufacturer: vendorId: 0x1387 productId: 0x6099 deviceProfileName: light-color-level - - id: "4999/28871" - deviceLabel: Govee Christmas String Lights 2 (Green Wire) 164ft/50m - vendorId: 0x1387 - productId: 0x70C7 - deviceProfileName: light-color-level + # - id: "4999/28871" + # deviceLabel: Govee Christmas String Lights 2 (Green Wire) 164ft/50m + # vendorId: 0x1387 + # productId: 0x70C7 + # deviceProfileName: light-color-level - id: "4999/28873" deviceLabel: Govee Christmas String Lights 2 (Green Wire) 328ft/100m vendorId: 0x1387 @@ -568,11 +568,11 @@ matterManufacturer: vendorId: 0x1387 productId: 0x6010 deviceProfileName: light-color-level - - id: "4999/28869" - deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 99ft/30m - vendorId: 0x1387 - productId: 0x70C5 - deviceProfileName: light-color-level + # - id: "4999/28869" + # deviceLabel: Govee Christmas String Lights 2 (Clear Wire) 99ft/30m + # vendorId: 0x1387 + # productId: 0x70C5 + # deviceProfileName: light-color-level - id: "4999/24607" deviceLabel: Govee Recessed Downlight 2 (6 inch) vendorId: 0x1387 @@ -618,11 +618,11 @@ matterManufacturer: vendorId: 0x1387 productId: 0x607C deviceProfileName: light-color-level - - id: "4999/26116" - deviceLabel: Govee HDMI 2.1 Sync Box 2 55-65 - vendorId: 0x1387 - productId: 0x6604 - deviceProfileName: light-color-level + # - id: "4999/26116" + # deviceLabel: Govee HDMI 2.1 Sync Box 2 55-65 + # vendorId: 0x1387 + # productId: 0x6604 + # deviceProfileName: light-color-level - id: "4999/24694" deviceLabel: Govee Floor Lamp Basic vendorId: 0x1387 @@ -653,11 +653,11 @@ matterManufacturer: vendorId: 0x1387 productId: 0x6020 deviceProfileName: light-color-level - - id: "4999/28868" - deviceLabel: Govee Christmas String Lights 2 (Green Wire) 66ft/20m - vendorId: 0x1387 - productId: 0x70C4 - deviceProfileName: light-color-level + # - id: "4999/28868" + # deviceLabel: Govee Christmas String Lights 2 (Green Wire) 66ft/20m + # vendorId: 0x1387 + # productId: 0x70C4 + # deviceProfileName: light-color-level - id: "4999/25046" deviceLabel: Govee Neon Rope Light 2 32.8ft/10m vendorId: 0x1387 @@ -713,11 +713,11 @@ matterManufacturer: vendorId: 0x1387 productId: 0x66A0 deviceProfileName: light-color-level - - id: "4999/26272" - deviceLabel: Govee TV Backlight 3 Pro 75-85 - vendorId: 0x1387 - productId: 0x66A0 - deviceProfileName: light-color-level + # - id: "4999/26272" + # deviceLabel: Govee TV Backlight 3 Pro 75-85 + # vendorId: 0x1387 + # productId: 0x66A0 + # deviceProfileName: light-color-level - id: "4999/28854" deviceLabel: Govee Curtain Lights Pro vendorId: 0x1387 From f4299ed95d0db938e82b61de6aefd4a03f21ad3d Mon Sep 17 00:00:00 2001 From: Steven Green Date: Sat, 13 Dec 2025 11:53:28 -0800 Subject: [PATCH 345/449] WWSTCERT-9475 eWeLink Zigbee Motion Sensor --- drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml index 28aaea8d33..e71ab0c5ac 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-motion-sensor/fingerprints.yml @@ -24,6 +24,11 @@ zigbeeManufacturer: manufacturer: eWeLink model: MSO1 deviceProfileName: motion-battery + - id: eWeLink/SNZB-03P + deviceLabel: Zigbee Motion Sensor + manufacturer: eWeLink + model: SNZB-03P + deviceProfileName: motion - id: "ORVIBO/895a2d80097f4ae2b2d40500d5e0" deviceLabel: Orvibo Motion Sensor manufacturer: ORVIBO From bb70244e0cf98fab138df6bbe261895ceedd2610 Mon Sep 17 00:00:00 2001 From: Script0803 <116477970+script0803@users.noreply.github.com> Date: Sun, 14 Dec 2025 04:35:22 +0800 Subject: [PATCH 346/449] Merge pull request #1999 from script0803/main WWSTCERT-8208 Add new Zigbee Power Meter Device --- .../zigbee-power-meter/fingerprints.yml | 40 ++ .../profiles/power-meter-1p.yml | 31 ++ .../profiles/power-meter-2p.yml | 42 +++ .../profiles/power-meter-3p.yml | 53 +++ .../src/bituo/can_handle.lua | 15 + .../src/bituo/fingerprints.lua | 15 + .../zigbee-power-meter/src/bituo/init.lua | 231 ++++++++++++ .../zigbee-power-meter/src/sub_drivers.lua | 1 + .../src/test/test_zigbee_power_meter_1p.lua | 225 +++++++++++ .../src/test/test_zigbee_power_meter_2p.lua | 281 ++++++++++++++ .../src/test/test_zigbee_power_meter_3p.lua | 355 ++++++++++++++++++ 11 files changed, 1289 insertions(+) create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml create mode 100644 drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua diff --git a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml index 37cf9df678..ec58259dd5 100644 --- a/drivers/SmartThings/zigbee-power-meter/fingerprints.yml +++ b/drivers/SmartThings/zigbee-power-meter/fingerprints.yml @@ -28,6 +28,46 @@ zigbeeManufacturer: manufacturer: ShinaSystem model: "PMM-300Z3" deviceProfileName: power-meter-consumption-report-sihas + - id: "BITUO TECHNIK/SPM01-E0" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SPM01-E0" + deviceProfileName: power-meter-1p + - id: "BITUO TECHNIK/SPM01X" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SPM01X" + deviceProfileName: power-meter-1p + - id: "BITUO TECHNIK/SDM02-E0" + deviceLabel: Energy Monitor 2PN + manufacturer: BITUO TECHNIK + model: "SDM02-E0" + deviceProfileName: power-meter-2p + - id: "BITUO TECHNIK/SDM02X" + deviceLabel: Energy Monitor 2PN + manufacturer: BITUO TECHNIK + model: "SDM02X" + deviceProfileName: power-meter-2p + - id: "BITUO TECHNIK/SPM02-E0" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SPM02-E0" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SPM02X" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SPM02X" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SDM01W" + deviceLabel: Energy Monitor 3PN + manufacturer: BITUO TECHNIK + model: "SDM01W" + deviceProfileName: power-meter-3p + - id: "BITUO TECHNIK/SDM01B" + deviceLabel: Energy Monitor 1PN + manufacturer: BITUO TECHNIK + model: "SDM01B" + deviceProfileName: power-meter-1p zigbeeGeneric: - id: "genericMeter" deviceLabel: Zigbee Meter diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml new file mode 100644 index 0000000000..7290794ffe --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-1p.yml @@ -0,0 +1,31 @@ +name: power-meter-1p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml new file mode 100644 index 0000000000..11f2dd412b --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-2p.yml @@ -0,0 +1,42 @@ +name: power-meter-2p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseB + label: Phase B + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml new file mode 100644 index 0000000000..20c824d092 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/profiles/power-meter-3p.yml @@ -0,0 +1,53 @@ +name: power-meter-3p +components: +- id: main + label: Total Forward Energy + capabilities: + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: TotalReverseEnergy + label: Total Reverse Energy + capabilities: + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseA + label: Phase A + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseB + label: Phase B + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter +- id: PhaseC + label: Phase C + capabilities: + - id: powerMeter + version: 1 + - id: currentMeasurement + version: 1 + - id: voltageMeasurement + version: 1 + categories: + - name: CurbPowerMeter diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua new file mode 100644 index 0000000000..a2124afdd6 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_bituo_power_meter = function(opts, driver, device) + local FINGERPRINTS = require("bituo.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_model() == fingerprint.model then + return true, require("bituo") + end + end + + return false +end + +return is_bituo_power_meter diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua new file mode 100644 index 0000000000..3909881854 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_POWER_METER_FINGERPRINTS = { + { mfr = "BITUO TECHNIK", model = "SPM01-E0" }, + { mfr = "BITUO TECHNIK", model = "SPM01X" }, + { mfr = "BITUO TECHNIK", model = "SDM02-E0" }, + { mfr = "BITUO TECHNIK", model = "SDM02X" }, + { mfr = "BITUO TECHNIK", model = "SPM02-E0" }, + { mfr = "BITUO TECHNIK", model = "SPM02X" }, + { mfr = "BITUO TECHNIK", model = "SDM01W" }, + { mfr = "BITUO TECHNIK", model = "SDM01B" } +} + +return ZIGBEE_POWER_METER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua b/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua new file mode 100644 index 0000000000..4bf5dbd359 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/bituo/init.lua @@ -0,0 +1,231 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local constants = require "st.zigbee.constants" +local clusters = require "st.zigbee.zcl.clusters" +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local configurations = require "configurations" + + +local PHASE_A_CONFIGURATION = { + { + cluster = SimpleMetering.ID, + attribute = SimpleMetering.attributes.CurrentSummationDelivered.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 0 + }, + { + cluster = SimpleMetering.ID, + attribute = 0x0001, + minimum_interval = 30, + maximum_interval = 120, + data_type = SimpleMetering.attributes.CurrentSummationDelivered.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePower.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePower.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltage.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltage.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrent.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrent.base_type, + reportable_change = 0 + } +} +local PHASE_B_CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePowerPhB.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltagePhB.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhB.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrentPhB.base_type, + reportable_change = 0 + }, +} +local PHASE_C_CONFIGURATION = { + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.ActivePowerPhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.ActivePowerPhC.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSVoltagePhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSVoltagePhC.base_type, + reportable_change = 0 + }, + { + cluster = ElectricalMeasurement.ID, + attribute = ElectricalMeasurement.attributes.RMSCurrentPhC.ID, + minimum_interval = 30, + maximum_interval = 120, + data_type = ElectricalMeasurement.attributes.RMSCurrentPhC.base_type, + reportable_change = 0 + } +} + +local function energy_handler(driver, device, value, zb_rx) + local multiplier = 1 + local divisor = 100 + local raw_value = value.value + local raw_value_kilowatts = raw_value * multiplier/divisor + + local offset = device:get_field(constants.ENERGY_METER_OFFSET) or 0 + if raw_value_kilowatts < 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}) + end + raw_value_kilowatts = raw_value_kilowatts - offset + + local raw_value_watts = raw_value_kilowatts*1000 + local delta_tick + local last_save_ticks = device:get_field("LAST_SAVE_TICK") + + if last_save_ticks == nil then last_save_ticks = 0 end + delta_tick = os.time() - last_save_ticks + + -- wwst energy certification : powerConsumptionReport capability values should be updated every 15 minutes. + -- Check if 15 minutes have passed since the reporting time of current_power_consumption. + if delta_tick >= 15*60 then + local delta_energy = 0.0 + 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_watts - current_power_consumption.energy, 0.0) + end + device:emit_event(capabilities.powerConsumptionReport.powerConsumption({energy = raw_value_watts, deltaEnergy = delta_energy })) -- the unit of these values should be 'Wh' + + local curr_save_tick = last_save_ticks + 15*60 -- Set the time to a regular interval by adding 15 minutes to the existing last_save_ticks. + -- If the time 15 minutes from now is less than the current time, set the current time as the last time. + if curr_save_tick + 15*60 < os.time() then + curr_save_tick = os.time() + end + device:set_field("LAST_SAVE_TICK", curr_save_tick, {persist = false}) + end + device:emit_event(capabilities.energyMeter.energy({value = raw_value_kilowatts, unit = "kWh"})) +end + +local function generic_handler_factory(component_name, capability, multiplier, divisor, unit) + return function(driver, device, value, zb_rx) + local component = device.profile.components[component_name] + if component ~= nil then + local raw_value = value.value * multiplier / divisor + device:emit_component_event(component, capability({value = raw_value, unit = unit})) + end + end +end + +local refresh = function(driver, device, cmd) + device:refresh() +end + +local function resetEnergyMeter(self, device) + device:send(clusters.OnOff.server.commands.On(device)) + -- Reset Power consumption + device:set_field(constants.ENERGY_METER_OFFSET, 0, {persist = true}) + device:set_field("LAST_SAVE_TICK", os.time(), {persist = false}) +end +local function do_configure(driver, device) + device:configure() + --device:send(device_management.build_bind_request(device, clusters.SimpleMetering.ID, driver.environment_info.hub_zigbee_eui)) + --device:send(device_management.build_bind_request(device, clusters.ElectricalMeasurement.ID, driver.environment_info.hub_zigbee_eui)) + device:refresh() +end + +local device_init = function(self, device) + for _, attribute in ipairs(PHASE_A_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + if string.find(device:get_model(), "SDM02") or string.find(device:get_model(), "SPM02") or string.find(device:get_model(), "SDM01W") then + for _, attribute in ipairs(PHASE_B_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + end + if string.find(device:get_model(), "SPM02") or string.find(device:get_model(), "SDM01W") then + for _, attribute in ipairs(PHASE_C_CONFIGURATION) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end + end +end + +local bituo_power_meter_handler = { + NAME = "bituo power meter handler", + lifecycle_handlers = { + init = configurations.power_reconfig_wrapper(device_init), + doConfigure = do_configure, + }, + zigbee_handlers = { + attr = { + [clusters.SimpleMetering.ID] = { + [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_handler, + [0x0001] = generic_handler_factory("TotalReverseEnergy", capabilities.energyMeter.energy, 1, 100, "kWh"), + }, + [clusters.ElectricalMeasurement.ID] = { + [ElectricalMeasurement.attributes.ActivePower.ID] = generic_handler_factory("PhaseA", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.ActivePowerPhB.ID] = generic_handler_factory("PhaseB", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.ActivePowerPhC.ID] = generic_handler_factory("PhaseC", capabilities.powerMeter.power, 1, 1, "W"), + [ElectricalMeasurement.attributes.RMSVoltage.ID] = generic_handler_factory("PhaseA", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSVoltagePhB.ID] = generic_handler_factory("PhaseB", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSVoltagePhC.ID] = generic_handler_factory("PhaseC", capabilities.voltageMeasurement.voltage, 1, 100, "V"), + [ElectricalMeasurement.attributes.RMSCurrent.ID] = generic_handler_factory("PhaseA", capabilities.currentMeasurement.current, 1, 100, "A"), + [ElectricalMeasurement.attributes.RMSCurrentPhB.ID] = generic_handler_factory("PhaseB", capabilities.currentMeasurement.current, 1, 100, "A"), + [ElectricalMeasurement.attributes.RMSCurrentPhC.ID] = generic_handler_factory("PhaseC", capabilities.currentMeasurement.current, 1, 100, "A") + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = resetEnergyMeter, + }, + }, + can_handle = require("bituo.can_handle"), +} + +return bituo_power_meter_handler diff --git a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua index 58ff064c9f..51b24aca32 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/sub_drivers.lua @@ -6,5 +6,6 @@ local sub_drivers = { lazy_load_if_possible("ezex"), lazy_load_if_possible("frient"), lazy_load_if_possible("shinasystems"), + lazy_load_if_possible("bituo"), } return sub_drivers 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 new file mode 100644 index 0000000000..5308e11ee2 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua @@ -0,0 +1,225 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + + +-- Mock out globals +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-1p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SPM01X", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +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( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + 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, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered: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, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file 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 new file mode 100644 index 0000000000..b85c955005 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua @@ -0,0 +1,281 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +-- 使用两相电能表配置文件 +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-2p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SDM02X", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +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( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhB:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhB:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhB:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + 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, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered: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, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file 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 new file mode 100644 index 0000000000..fad26aad40 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua @@ -0,0 +1,355 @@ +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter-3p.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "BITUO TECHNIK", + model = "SDM01W", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +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( + "SimpleMetering event should be handled by powerConsumptionReport capability", + function() + test.timer.__create_and_queue_test_time_advance_timer(15*60, "oneshot") + -- #1 : 15 minutes have passed + test.mock_time.advance_time(15*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,150) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + -- #2 : Not even 15 minutes passed + test.wait_for_events() + test.mock_time.advance_time(1*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,170) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.7, unit = "kWh"})) + ) + -- #3 : 15 minutes have passed + test.wait_for_events() + test.mock_time.advance_time(14*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device,200) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 2000.0, deltaEnergy = 500.0 })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 2.0, unit = "kWh"})) + ) + end +) + +test.register_message_test( + "ActivePower Report should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrent:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseA should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltage:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseA", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report for PhaseB should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhB:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhB:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseB should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhB:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseB", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_message_test( + "ActivePower Report for PhaseC should be handled. Sensor value is in W, capability attribute value is in hectowatts", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.ActivePowerPhC:build_test_attr_report(mock_device, + 27) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.powerMeter.power({ value = 27.0, unit = "W" })) + } + } +) + +test.register_message_test( + "RMSCurrent Report for PhaseC should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSCurrentPhC:build_test_attr_report(mock_device, + 34) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.currentMeasurement.current({ value = 0.34, unit = "A" })) + } + } +) + +test.register_message_test( + "RMSVoltage Report for PhaseC should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ElectricalMeasurement.attributes.RMSVoltagePhC:build_test_attr_report(mock_device, + 22000) }, + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("PhaseC", capabilities.voltageMeasurement.voltage({ value = 220.0, unit = "V" })) + } + } +) + +test.register_coroutine_test( + "Device configure lifecycle event should configure device properly", + function() + 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, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:configure_reporting(mock_device, 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.configure_reporting(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001), data_types.ZigbeeDataType(SimpleMetering.attributes.CurrentSummationDelivered.base_type.ID), 30, 120, 0) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered: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, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhB:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePowerPhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltagePhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrentPhC:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() \ No newline at end of file From 24234be689f4434beb7f56765df277a41ed02d74 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 15 Dec 2025 10:15:45 -0800 Subject: [PATCH 347/449] adjust base profile for ikea bilresa scroll --- drivers/SmartThings/matter-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index a53fc144a6..5e65aa24fe 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -847,7 +847,7 @@ matterManufacturer: deviceLabel: BILRESA scroll wheel vendorId: 0x117C productId: 0x8000 - deviceProfileName: 2-button-battery + deviceProfileName: ikea-scroll - id: "4476/32769" deviceLabel: BILRESA dual button vendorId: 0x117C From cba8c07d0a530d1f877a57b1f1d35cc673513d79 Mon Sep 17 00:00:00 2001 From: haedo-doo Date: Tue, 16 Dec 2025 10:30:14 +0900 Subject: [PATCH 348/449] a follow-up PR for #2468 that removes the no longer needed code from init --- .../SmartThings/zigbee-switch/src/aqara-light/init.lua | 9 --------- .../zigbee-switch/src/test/test_aqara_led_bulb.lua | 3 +-- .../zigbee-switch/src/test/test_aqara_light.lua | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 06d4bf1cc7..2192716b21 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -54,18 +54,9 @@ local function set_level_handler(driver, device, cmd) device:send(Level.commands.MoveToLevelWithOnOff(device, level, dimming_rate)) end -local function init(self, device) - local value = { minimum = 2700, maximum = 6000 } - if device:get_model() == "lumi.light.cwacn1" then - value.maximum = 6500 - end - emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) -end - local aqara_light_handler = { NAME = "Aqara Light Handler", lifecycle_handlers = { - init = init, added = device_added, doConfigure = do_configure }, 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 b2f9359850..ce4bb13957 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 @@ -38,7 +38,6 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -52,7 +51,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) 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 940131b3e1..c521ba3e44 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -40,7 +40,6 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -54,7 +53,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) From f89ad93d9518fff574d4dd5d8c3151620d50a6c1 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 16 Dec 2025 09:35:21 -0600 Subject: [PATCH 349/449] CHAD-17161: lazy loading of matter-sensor sub-drivers --- .../PressureMeasurement/init.lua | 17 +++-------------- .../server/attributes/AcceptedCommandList.lua | 17 +++-------------- .../server/attributes/AttributeList.lua | 17 +++-------------- .../server/attributes/EventList.lua | 17 +++-------------- .../server/attributes/MaxMeasuredValue.lua | 17 +++-------------- .../server/attributes/MaxScaledValue.lua | 17 +++-------------- .../server/attributes/MeasuredValue.lua | 17 +++-------------- .../server/attributes/MinMeasuredValue.lua | 17 +++-------------- .../server/attributes/MinScaledValue.lua | 17 +++-------------- .../server/attributes/Scale.lua | 17 +++-------------- .../server/attributes/ScaledTolerance.lua | 17 +++-------------- .../server/attributes/ScaledValue.lua | 17 +++-------------- .../server/attributes/Tolerance.lua | 17 +++-------------- .../server/attributes/init.lua | 17 +++-------------- .../server/commands/init.lua | 17 +++-------------- .../PressureMeasurement/server/events/init.lua | 17 +++-------------- .../PressureMeasurement/types/Feature.lua | 17 +++-------------- .../PressureMeasurement/types/init.lua | 17 +++-------------- drivers/SmartThings/matter-sensor/src/init.lua | 8 ++------ .../matter-sensor/src/lazy_load_subdriver.lua | 14 ++++++++++++++ .../matter-sensor/src/sub_drivers.lua | 10 ++++++++++ .../air_quality_sensor_utils/utils.lua | 14 +------------- .../air_quality_sensor/can_handle.lua | 17 +++++++++++++++++ .../sub_drivers/air_quality_sensor/init.lua | 4 ++-- .../bosch_button_contact/can_handle.lua | 16 ++++++++++++++++ .../sub_drivers/bosch_button_contact/init.lua | 17 ++--------------- .../sub_drivers/smoke_co_alarm/can_handle.lua | 17 +++++++++++++++++ .../src/sub_drivers/smoke_co_alarm/init.lua | 18 +++--------------- 28 files changed, 138 insertions(+), 303 deletions(-) create mode 100644 drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua create mode 100644 drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua index 8b1661152a..42b058eb02 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -141,4 +131,3 @@ setmetatable(PressureMeasurement.events, event_helper_mt) setmetatable(PressureMeasurement, {__index = cluster_base}) return PressureMeasurement - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua index ee9fad2526..dc301eba35 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AcceptedCommandList.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AcceptedCommandList, {__call = AcceptedCommandList.new_value, __index = AcceptedCommandList.base_type}) return AcceptedCommandList - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua index d4d85b00a0..deba9d030f 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/AttributeList.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) return AttributeList - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua index b3dc7bf94c..f6040a17a0 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/EventList.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(EventList, {__call = EventList.new_value, __index = EventList.base_type}) return EventList - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua index 2484d99ef3..7bbf34d9e5 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxMeasuredValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MaxMeasuredValue, {__call = MaxMeasuredValue.new_value, __index = MaxMeasuredValue.base_type}) return MaxMeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua index 56fe3132f9..91e5f4edc4 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MaxScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MaxScaledValue, {__call = MaxScaledValue.new_value, __index = MaxScaledValue.base_type}) return MaxScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua index 7d850f1a7c..b7c0484722 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MeasuredValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MeasuredValue, {__call = MeasuredValue.new_value, __index = MeasuredValue.base_type}) return MeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua index 35ec62854f..ed4e421c77 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinMeasuredValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MinMeasuredValue, {__call = MinMeasuredValue.new_value, __index = MinMeasuredValue.base_type}) return MinMeasuredValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua index 97bb0d44df..552ae6160a 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/MinScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(MinScaledValue, {__call = MinScaledValue.new_value, __index = MinScaledValue.base_type}) return MinScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua index 5533e4e486..085153bd7a 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Scale.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(Scale, {__call = Scale.new_value, __index = Scale.base_type}) return Scale - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua index d00d2a4195..4c14d72ea3 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledTolerance.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ScaledTolerance, {__call = ScaledTolerance.new_value, __index = ScaledTolerance.base_type}) return ScaledTolerance - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua index f2e513477b..28cfe27a7a 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/ScaledValue.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ScaledValue, {__call = ScaledValue.new_value, __index = ScaledValue.base_type}) return ScaledValue - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua index 55a39514a3..4d1b45ee48 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/Tolerance.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(Tolerance, {__call = Tolerance.new_value, __index = Tolerance.base_type}) return Tolerance - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua index 51844f7b7f..ebd4480609 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/attributes/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -51,4 +41,3 @@ end setmetatable(PressureMeasurementServerAttributes, attr_mt) return PressureMeasurementServerAttributes - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua index 94f1c0849a..db5c3d9d45 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/commands/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -38,4 +28,3 @@ end setmetatable(PressureMeasurementServerCommands, command_mt) return PressureMeasurementServerCommands - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua index fa444e129a..d96e0d7438 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/server/events/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -39,4 +29,3 @@ end setmetatable(PressureMeasurementEvents, event_mt) return PressureMeasurementEvents - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua index 5f5f070b7d..814c5ef226 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/Feature.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -78,4 +68,3 @@ end setmetatable(Feature, new_mt) return Feature - diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua index f83681fdb8..4f0c8b6bdc 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/PressureMeasurement/types/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -32,4 +22,3 @@ local PressureMeasurementTypes = {} setmetatable(PressureMeasurementTypes, types_mt) return PressureMeasurementTypes - diff --git a/drivers/SmartThings/matter-sensor/src/init.lua b/drivers/SmartThings/matter-sensor/src/init.lua index 4f6b204aea..73e34fcd56 100644 --- a/drivers/SmartThings/matter-sensor/src/init.lua +++ b/drivers/SmartThings/matter-sensor/src/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -295,11 +295,7 @@ local matter_driver_template = { capabilities.hardwareFault, capabilities.flowMeasurement, }, - sub_drivers = { - require("sub_drivers.air_quality_sensor"), - require("sub_drivers.smoke_co_alarm"), - require("sub_drivers.bosch_button_contact") - } + sub_drivers = require("sub_drivers"), } local matter_driver = MatterDriver("matter-sensor", matter_driver_template) diff --git a/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..3bf4c46f73 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("sub_drivers.air_quality_sensor"), + lazy_load_if_possible("sub_drivers.smoke_co_alarm"), + lazy_load_if_possible("sub_drivers.bosch_button_contact"), +} +return sub_drivers 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 2462ced55d..1a36f5f920 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,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -9,18 +9,6 @@ local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils. local AirQualitySensorUtils = {} -function AirQualitySensorUtils.is_matter_air_quality_sensor(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then - return true - end - end - end - - return false - end - function AirQualitySensorUtils.supports_capability_by_id_modular(device, capability, component) if not device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then device.log.warn_with({hub_logs = true}, "Device has overriden supports_capability_by_id, but does not have supported capabilities set.") diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua new file mode 100644 index 0000000000..9c1a1ab762 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_air_quality_sensor(opts, driver, device) + local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == fields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID then + return true, require("sub_drivers.air_quality_sensor") + end + end + end + + return false +end + +return is_matter_air_quality_sensor 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 a0427ac665..61280fd107 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 @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local version = require "version" @@ -148,7 +148,7 @@ local matter_air_quality_sensor_handler = { } } }, - can_handle = aqs_utils.is_matter_air_quality_sensor + can_handle = require("sub_drivers.air_quality_sensor.can_handle") } return matter_air_quality_sensor_handler diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua new file mode 100644 index 0000000000..9c679f3607 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_bosch_button_contact(opts, driver, device) + local device_lib = require "st.device" + local BOSCH_VENDOR_ID = 0x1209 + local BOSCH_PRODUCT_ID = 0x3015 + if device.network_type == device_lib.NETWORK_TYPE_MATTER and + device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and + device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then + return true, require("sub_drivers.bosch_button_contact") + end + return false +end + +return is_bosch_button_contact diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua index 9b4635d87c..cbbd71cf53 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/bosch_button_contact/init.lua @@ -1,26 +1,13 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" -local device_lib = require "st.device" local lua_socket = require "socket" local log = require "log" local START_BUTTON_PRESS = "__start_button_press" -local BOSCH_VENDOR_ID = 0x1209 -local BOSCH_PRODUCT_ID = 0x3015 - -local function is_bosch_button_contact(opts, driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == BOSCH_VENDOR_ID and - device.manufacturer_info.product_id == BOSCH_PRODUCT_ID then - return true - end - return false -end - local function get_field_for_endpoint(device, field, endpoint) return device:get_field(string.format("%s_%d", field, endpoint)) end @@ -145,7 +132,7 @@ local Bosch_Button_Contact_Sensor = { } }, }, - can_handle = is_bosch_button_contact, + can_handle = require("sub_drivers.bosch_button_contact.can_handle"), } return Bosch_Button_Contact_Sensor diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua new file mode 100644 index 0000000000..31bd7e025b --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_smoke_co_alarm(opts, driver, device) + local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == SMOKE_CO_ALARM_DEVICE_TYPE_ID then + return true, require("sub_drivers.smoke_co_alarm") + end + end + end + + return false +end + +return is_matter_smoke_co_alarm diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua index 374bbd5ad9..8f8653b936 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/smoke_co_alarm/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -19,23 +19,11 @@ end local smoke_co_alarm_utils = {} local CARBON_MONOXIDE_MEASUREMENT_UNIT = "CarbonMonoxideConcentrationMeasurement_unit" -local SMOKE_CO_ALARM_DEVICE_TYPE_ID = 0x0076 local HardwareFaultAlert = "__HardwareFaultAlert" local BatteryAlert = "__BatteryAlert" local BatteryLevel = "__BatteryLevel" -function smoke_co_alarm_utils.is_matter_smoke_co_alarm(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == SMOKE_CO_ALARM_DEVICE_TYPE_ID then - return true - end - end - end - - return false -end local supported_profiles = { @@ -271,7 +259,7 @@ local matter_smoke_co_alarm_handler = { }, }, }, - can_handle = smoke_co_alarm_utils.is_matter_smoke_co_alarm + can_handle = require("sub_drivers.smoke_co_alarm.can_handle") } -return matter_smoke_co_alarm_handler \ No newline at end of file +return matter_smoke_co_alarm_handler From 5da406dc72153d4fe05ccce92ba99faa4cfe59b7 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 15 Dec 2025 15:22:12 -0600 Subject: [PATCH 350/449] CHAD-17152: Lazy loading of matter-appliance sub-drivers --- .../ActivatedCarbonFilterMonitoring/init.lua | 4 + .../server/attributes/AttributeList.lua | 17 +- .../server/attributes/ChangeIndication.lua | 17 +- .../types/init.lua | 3 + .../src/DishwasherAlarm/init.lua | 4 + .../src/DishwasherAlarm/types/init.lua | 3 + .../src/DishwasherMode/init.lua | 4 + .../src/DishwasherMode/types/init.lua | 3 + .../src/HepaFilterMonitoring/init.lua | 4 + .../server/attributes/AttributeList.lua | 17 +- .../server/attributes/ChangeIndication.lua | 17 +- .../src/HepaFilterMonitoring/types/init.lua | 3 + .../src/LaundryWasherControls/init.lua | 4 + .../src/LaundryWasherControls/types/init.lua | 3 + .../src/LaundryWasherMode/init.lua | 4 + .../src/LaundryWasherMode/types/init.lua | 3 + .../src/MicrowaveOvenControl/init.lua | 6 +- .../src/MicrowaveOvenControl/types/init.lua | 3 + .../src/MicrowaveOvenMode/init.lua | 4 + .../src/MicrowaveOvenMode/types/init.lua | 3 + .../src/OperationalState/init.lua | 4 + .../src/OperationalState/types/init.lua | 3 + .../matter-appliance/src/OvenMode/init.lua | 4 + .../src/OvenMode/types/init.lua | 3 + .../src/RefrigeratorAlarm/init.lua | 4 + .../src/RefrigeratorAlarm/types/init.lua | 3 + .../init.lua | 4 + .../types/init.lua | 3 + .../src/TemperatureControl/init.lua | 4 + .../src/TemperatureControl/types/init.lua | 3 + .../matter-appliance/src/common-utils.lua | 16 +- .../src/embedded-cluster-utils.lua | 16 +- .../SmartThings/matter-appliance/src/init.lua | 25 +-- .../src/lazy_load_subdriver.lua | 14 ++ .../src/matter-cook-top/can_handle.lua | 21 +++ .../src/matter-cook-top/init.lua | 170 ++++++++---------- .../src/matter-dishwasher/can_handle.lua | 17 ++ .../src/matter-dishwasher/init.lua | 29 +-- .../src/matter-extractor-hood/can_handle.lua | 16 ++ .../src/matter-extractor-hood/init.lua | 29 +-- .../src/matter-laundry/can_handle.lua | 19 ++ .../src/matter-laundry/init.lua | 33 +--- .../src/matter-microwave-oven/can_handle.lua | 16 ++ .../src/matter-microwave-oven/init.lua | 29 +-- .../src/matter-oven/can_handle.lua | 14 ++ .../matter-appliance/src/matter-oven/init.lua | 26 +-- .../src/matter-refrigerator/can_handle.lua | 16 ++ .../src/matter-refrigerator/init.lua | 30 +--- .../matter-appliance/src/sub_drivers.lua | 14 ++ .../src/test/test_cook_top.lua | 18 +- .../src/test/test_dishwasher.lua | 16 +- .../src/test/test_extractor_hood.lua | 16 +- .../src/test/test_laundry_dryer.lua | 16 +- .../src/test/test_laundry_washer.lua | 16 +- .../src/test/test_matter_appliance_rpc_5.lua | 16 +- .../src/test/test_microwave_oven.lua | 16 +- .../matter-appliance/src/test/test_oven.lua | 16 +- .../src/test/test_refrigerator.lua | 16 +- 58 files changed, 386 insertions(+), 471 deletions(-) create mode 100644 drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua create mode 100644 drivers/SmartThings/matter-appliance/src/sub_drivers.lua diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua index c7057f82e4..fff466fea1 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local ActivatedCarbonFilterMonitoringServerAttributes = require "ActivatedCarbonFilterMonitoring.server.attributes" local ActivatedCarbonFilterMonitoringTypes = require "ActivatedCarbonFilterMonitoring.types" diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua index ef3fb00a6a..9f34e51dbf 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/AttributeList.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) return AttributeList - diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua index dd6fe1ecfb..71170e772f 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) return ChangeIndication - diff --git a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua index 2ff8e6e89a..80e6e6b86a 100644 --- a/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/ActivatedCarbonFilterMonitoring/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua index 9a6be67486..382197bc60 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DishwasherAlarmServerAttributes = require "DishwasherAlarm.server.attributes" local DishwasherAlarmTypes = require "DishwasherAlarm.types" diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua index d5c4bba1fb..3ddec58dcc 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherAlarm/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua index 913f001fdb..0bb22e1884 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DishwasherModeServerAttributes = require "DishwasherMode.server.attributes" local DishwasherModeServerCommands = require "DishwasherMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua index dd07b2f752..34d510160e 100644 --- a/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/DishwasherMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua index 21795104b7..b6df5d554c 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local HepaFilterMonitoringServerAttributes = require "HepaFilterMonitoring.server.attributes" local HepaFilterMonitoringTypes = require "HepaFilterMonitoring.types" diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua index c4f817e428..6fdd2e5350 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/AttributeList.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -123,4 +113,3 @@ end setmetatable(AttributeList, {__call = AttributeList.new_value, __index = AttributeList.base_type}) return AttributeList - diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua index 06a80153e3..b205026e1b 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/server/attributes/ChangeIndication.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- DO NOT EDIT: this code is automatically generated by ZCL Advanced Platform generator. @@ -113,4 +103,3 @@ end setmetatable(ChangeIndication, {__call = ChangeIndication.new_value, __index = ChangeIndication.base_type}) return ChangeIndication - diff --git a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua index 77aca088ff..f4eea9ee26 100644 --- a/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/HepaFilterMonitoring/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua index 63bf4168e7..c6ff01850b 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local LaundryWasherControlsServerAttributes = require "LaundryWasherControls.server.attributes" local LaundryWasherControlsTypes = require "LaundryWasherControls.types" diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua index e8a2f0ac53..4ba9f25726 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherControls/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua index aa030e58cc..e64bde94a0 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local LaundryWasherModeServerAttributes = require "LaundryWasherMode.server.attributes" local LaundryWasherModeServerCommands = require "LaundryWasherMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua index 057fe27dbe..d201d4ba36 100644 --- a/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/LaundryWasherMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua index 697aa3fd05..5c333dea1e 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local MicrowaveOvenControlServerAttributes = require "MicrowaveOvenControl.server.attributes" local MicrowaveOvenControlServerCommands = require "MicrowaveOvenControl.server.commands" @@ -81,4 +85,4 @@ setmetatable(MicrowaveOvenControl.commands, command_helper_mt) setmetatable(MicrowaveOvenControl, {__index = cluster_base}) -return MicrowaveOvenControl \ No newline at end of file +return MicrowaveOvenControl diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua index 29c27b80ae..1d11195ee2 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenControl/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua index fdbbf4c443..dca8ff3ed5 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local MicrowaveOvenModeServerAttributes = require "MicrowaveOvenMode.server.attributes" local MicrowaveOvenModeTypes = require "MicrowaveOvenMode.types" diff --git a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua index 412d69cabd..e67593141b 100644 --- a/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/MicrowaveOvenMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua b/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua index 5239cc1cfe..7be274fb8d 100644 --- a/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OperationalState/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local OperationalStateServerAttributes = require "OperationalState.server.attributes" local OperationalStateServerCommands = require "OperationalState.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua b/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua index 8c735ed06a..18bbca0824 100644 --- a/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OperationalState/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua index 76e1817dc1..4fc7c11376 100644 --- a/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local OvenModeServerAttributes = require "OvenMode.server.attributes" local OvenModeServerCommands = require "OvenMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua index 8bd0777339..b3565aaa2b 100644 --- a/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/OvenMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua index a03452193c..1c9610b520 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local RefrigeratorAlarmServerAttributes = require "RefrigeratorAlarm.server.attributes" local RefrigeratorAlarmTypes = require "RefrigeratorAlarm.types" diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua index 111371cb52..d8ebdc1ed3 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAlarm/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua index f85f5e4e31..345b7b57b1 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local RefrigeratorAndTemperatureControlledCabinetModeServerAttributes = require "RefrigeratorAndTemperatureControlledCabinetMode.server.attributes" local RefrigeratorAndTemperatureControlledCabinetModeServerCommands = require "RefrigeratorAndTemperatureControlledCabinetMode.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua index d03e0d01d7..0079c8e25e 100644 --- a/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/RefrigeratorAndTemperatureControlledCabinetMode/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua b/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua index 2d453d8662..2ab1c94056 100644 --- a/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua +++ b/drivers/SmartThings/matter-appliance/src/TemperatureControl/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local TemperatureControlServerAttributes = require "TemperatureControl.server.attributes" local TemperatureControlServerCommands = require "TemperatureControl.server.commands" diff --git a/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua b/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua index feeafbc78b..53c77d6ff9 100644 --- a/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua +++ b/drivers/SmartThings/matter-appliance/src/TemperatureControl/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) diff --git a/drivers/SmartThings/matter-appliance/src/common-utils.lua b/drivers/SmartThings/matter-appliance/src/common-utils.lua index 1250d40912..d35ee2b6fc 100644 --- a/drivers/SmartThings/matter-appliance/src/common-utils.lua +++ b/drivers/SmartThings/matter-appliance/src/common-utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" diff --git a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua index 52d9909dff..5a5a89af73 100644 --- a/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua +++ b/drivers/SmartThings/matter-appliance/src/embedded-cluster-utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.matter.clusters" local utils = require "st.utils" diff --git a/drivers/SmartThings/matter-appliance/src/init.lua b/drivers/SmartThings/matter-appliance/src/init.lua index d107a48fc5..044da51e7c 100644 --- a/drivers/SmartThings/matter-appliance/src/init.lua +++ b/drivers/SmartThings/matter-appliance/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local MatterDriver = require "st.matter.driver" local capabilities = require "st.capabilities" @@ -307,15 +296,7 @@ local matter_driver_template = { capabilities.fanSpeedPercent, capabilities.windMode }, - sub_drivers = { - require("matter-cook-top"), - require("matter-dishwasher"), - require("matter-extractor-hood"), - require("matter-laundry"), - require("matter-microwave-oven"), - require("matter-oven"), - require("matter-refrigerator") - } + sub_drivers = require("sub_drivers"), } local matter_driver = MatterDriver("matter-appliance", matter_driver_template) diff --git a/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua new file mode 100644 index 0000000000..d2899d279a --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-cook-top/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_cook_top_device(opts, driver, device, ...) + local common_utils = require "common-utils" + local COOK_TOP_DEVICE_TYPE_ID = 0x0078 + local OVEN_DEVICE_ID = 0x007B + + local cook_top_eps = common_utils.get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID) + local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) + -- we want to skip lifecycle events in cases where the device is an oven with a composed cook-top device + if (#oven_eps > 0) and opts.dispatcher_class == "DeviceLifecycleDispatcher" then + return false + end + if #cook_top_eps > 0 then + return true, require("matter-cook-top") + end + return false +end + +return is_cook_top_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua index 0a8f9f3182..d0ce48dc18 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-cook-top/init.lua @@ -1,97 +1,73 @@ --- Copyright 2025 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 clusters = require "st.matter.clusters" -local common_utils = require "common-utils" -local embedded_cluster_utils = require "embedded-cluster-utils" -local version = require "version" - -if version.api < 10 then - clusters.TemperatureControl = require "TemperatureControl" -end - -local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 -local COOK_TOP_DEVICE_TYPE_ID = 0x0078 -local OVEN_DEVICE_ID = 0x007B - -local function table_contains(tab, val) - for _, tab_val in ipairs(tab) do - if tab_val == val then - return true - end - end - return false -end - -local function device_added(driver, device) - local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) - local componentToEndpointMap = { - ["cookSurfaceOne"] = cook_surface_endpoints[1], - ["cookSurfaceTwo"] = cook_surface_endpoints[2] - } - device:set_field(common_utils.COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) -end - -local function do_configure(driver, device) - local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) - - local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, - { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) - - local profile_name - if #cook_surface_endpoints > 0 then - profile_name = "cook-surface-one" - if table_contains(tl_eps, cook_surface_endpoints[1]) then - profile_name = profile_name .. "-tl" - end - - -- we only support up to two cook surfaces - if #cook_surface_endpoints > 1 then - profile_name = profile_name .. "-cook-surface-two" - if table_contains(tl_eps, cook_surface_endpoints[2]) then - profile_name = profile_name .. "-tl" - end - end - end - - if profile_name then - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({ profile = profile_name }) - end -end - -local function is_cook_top_device(opts, driver, device, ...) - local cook_top_eps = common_utils.get_endpoints_for_dt(device, COOK_TOP_DEVICE_TYPE_ID) - local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) - -- we want to skip lifecycle events in cases where the device is an oven with a composed cook-top device - if (#oven_eps > 0) and opts.dispatcher_class == "DeviceLifecycleDispatcher" then - return false - end - if #cook_top_eps > 0 then - return true - end - return false -end - --- Matter Handlers -- -local matter_cook_top_handler = { - NAME = "matter-cook-top", - lifecycle_handlers = { - added = device_added, - doConfigure = do_configure - }, - can_handle = is_cook_top_device -} - -return matter_cook_top_handler +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local clusters = require "st.matter.clusters" +local common_utils = require "common-utils" +local embedded_cluster_utils = require "embedded-cluster-utils" +local version = require "version" + +if version.api < 10 then + clusters.TemperatureControl = require "TemperatureControl" +end + +local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 + +local function table_contains(tab, val) + for _, tab_val in ipairs(tab) do + if tab_val == val then + return true + end + end + return false +end + +local function device_added(driver, device) + local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + local componentToEndpointMap = { + ["cookSurfaceOne"] = cook_surface_endpoints[1], + ["cookSurfaceTwo"] = cook_surface_endpoints[2] + } + device:set_field(common_utils.COMPONENT_TO_ENDPOINT_MAP, componentToEndpointMap, { persist = true }) +end + +local function do_configure(driver, device) + local cook_surface_endpoints = common_utils.get_endpoints_for_dt(device, COOK_SURFACE_DEVICE_TYPE_ID) + + local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, + { feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL }) + + local profile_name + if #cook_surface_endpoints > 0 then + profile_name = "cook-surface-one" + if table_contains(tl_eps, cook_surface_endpoints[1]) then + profile_name = profile_name .. "-tl" + end + + -- we only support up to two cook surfaces + if #cook_surface_endpoints > 1 then + profile_name = profile_name .. "-cook-surface-two" + if table_contains(tl_eps, cook_surface_endpoints[2]) then + profile_name = profile_name .. "-tl" + end + end + end + + if profile_name then + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({ profile = profile_name }) + end +end + + +-- Matter Handlers -- +local matter_cook_top_handler = { + NAME = "matter-cook-top", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + can_handle = require("matter-cook-top.can_handle"), +} + +return matter_cook_top_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua new file mode 100644 index 0000000000..37226f3f3e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local function is_matter_dishwasher(opts, driver, device) + local DISHWASHER_DEVICE_TYPE_ID = 0x0075 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == DISHWASHER_DEVICE_TYPE_ID then + return true, require("matter-dishwasher") + end + end + end + return false +end + +return is_matter_dishwasher diff --git a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua index 96339fafc5..2e01afe7fb 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-dishwasher/init.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -18,8 +7,6 @@ local common_utils = require "common-utils" local embedded_cluster_utils = require "embedded-cluster-utils" local version = require "version" -local DISHWASHER_DEVICE_TYPE_ID = 0x0075 - if version.api < 10 then clusters.DishwasherAlarm = require "DishwasherAlarm" clusters.DishwasherMode = require "DishwasherMode" @@ -36,16 +23,6 @@ local OPERATIONAL_STATE_COMMAND_MAP = { local SUPPORTED_DISHWASHER_MODES = "__supported_dishwasher_modes" -local function is_matter_dishwasher(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == DISHWASHER_DEVICE_TYPE_ID then - return true - end - end - end - return false -end -- Lifecycle Handlers -- local function do_configure(driver, device) @@ -267,7 +244,7 @@ local matter_dishwasher_handler = { [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume } }, - can_handle = is_matter_dishwasher + can_handle = require("matter-dishwasher.can_handle"), } return matter_dishwasher_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua new file mode 100644 index 0000000000..d8ec2f9afd --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_extractor_hood(opts, driver, device) + local EXTRACTOR_HOOD_DEVICE_TYPE_ID = 0x007A + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == EXTRACTOR_HOOD_DEVICE_TYPE_ID then + return true, require("matter-extractor-hood") + end + end + end + return false +end + +return is_matter_extractor_hood diff --git a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua index 57eef3ac94..f83681e726 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-extractor-hood/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -77,17 +67,6 @@ local function do_configure(driver, device) end -- Matter Handlers -- -local function is_matter_extractor_hood(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == EXTRACTOR_HOOD_DEVICE_TYPE_ID then - return true - end - end - end - return false -end - local function fan_mode_handler(driver, device, ib, response) if ib.data.value == clusters.FanControl.attributes.FanMode.OFF then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanMode.fanMode.off()) @@ -309,7 +288,7 @@ local matter_extractor_hood_handler = { [capabilities.windMode.commands.setWindMode.NAME] = set_wind_mode } }, - can_handle = is_matter_extractor_hood + can_handle = require("matter-extractor-hood.can_handle"), } return matter_extractor_hood_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua new file mode 100644 index 0000000000..dd71f12e9e --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-laundry/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_laundry_device(opts, driver, device) + local LAUNDRY_WASHER_DEVICE_TYPE_ID = 0x0073 + local LAUNDRY_DRYER_DEVICE_TYPE_ID = 0x007C + local LAUNDRY_DEVICE_TYPE_ID= "__laundry_device_type_id" + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID or dt.device_type_id == LAUNDRY_DRYER_DEVICE_TYPE_ID then + device:set_field(LAUNDRY_DEVICE_TYPE_ID, dt.device_type_id, {persist = true}) + return dt.device_type_id, require("matter-laundry") + end + end + end + return false +end + +return is_matter_laundry_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua index 8c49291da0..26b9efb606 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-laundry/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -47,23 +37,12 @@ local SUPPORTED_LAUNDRY_WASHER_MODES = "__supported_laundry_washer_modes" local SUPPORTED_LAUNDRY_WASHER_SPIN_SPEEDS = "__supported_laundry_spin_speeds" local SUPPORTED_LAUNDRY_WASHER_RINSES = "__supported_laundry_washer_rinses" -local function is_matter_laundry_device(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == LAUNDRY_WASHER_DEVICE_TYPE_ID or dt.device_type_id == LAUNDRY_DRYER_DEVICE_TYPE_ID then - device:set_field(LAUNDRY_DEVICE_TYPE_ID, dt.device_type_id, {persist = true}) - return dt.device_type_id - end - end - end - return false -end - -- Lifecycle Handlers -- local function do_configure(driver, device) local tn_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_NUMBER}) local tl_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureControl.ID, {feature_bitmap = clusters.TemperatureControl.types.Feature.TEMPERATURE_LEVEL}) - local device_type = is_matter_laundry_device({}, driver, device) + local is_matter_laundry_device = require("matter-laundry.can_handle") + local device_type, _ = is_matter_laundry_device({}, driver, device) local profile_name = "laundry" if (device_type == LAUNDRY_WASHER_DEVICE_TYPE_ID) then profile_name = profile_name.."-washer" @@ -316,7 +295,7 @@ local matter_laundry_handler = { [capabilities.operationalState.commands.resume.NAME] = handle_operational_state_resume } }, - can_handle = is_matter_laundry_device + can_handle = require("matter-laundry.can_handle"), } return matter_laundry_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua new file mode 100644 index 0000000000..ebdda29863 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_mircowave_oven(opts, driver, device) + local MICROWAVE_OVEN_DEVICE_TYPE_ID = 0x0079 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == MICROWAVE_OVEN_DEVICE_TYPE_ID then + return true, require("matter-microwave-oven") + end + end + end + return false +end + +return is_matter_mircowave_oven diff --git a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua index 6ebd6a4c69..342723656a 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-microwave-oven/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -34,7 +24,6 @@ local OPERATIONAL_STATE_COMMAND_MAP = { [clusters.OperationalState.commands.Resume.ID] = "resume", } -local MICROWAVE_OVEN_DEVICE_TYPE_ID = 0x0079 local DEFAULT_COOKING_MODE = 0 local DEFAULT_COOKING_TIME = 30 local MICROWAVE_OVEN_SUPPORTED_MODES_KEY = "__microwave_oven_supported_modes__" @@ -45,16 +34,6 @@ local function device_init(driver, device) device:send(clusters.MicrowaveOvenControl.attributes.MaxCookTime:read(device, device.MATTER_DEFAULT_ENDPOINT)) end -local function is_matter_mircowave_oven(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == MICROWAVE_OVEN_DEVICE_TYPE_ID then - return true - end - end - end - return false -end local function get_last_set_cooking_parameters(device) local cookingTime = device:get_latest_state("main", capabilities.cookTime.ID, capabilities.cookTime.cookTime.NAME) or DEFAULT_COOKING_TIME @@ -271,7 +250,7 @@ local matter_microwave_oven = { capabilities.mode, capabilities.cookTime }, - can_handle = is_matter_mircowave_oven + can_handle = require("matter-microwave-oven.can_handle"), } return matter_microwave_oven diff --git a/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua new file mode 100644 index 0000000000..a5bb61f9b8 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-oven/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_oven_device(opts, driver, device) + local common_utils = require "common-utils" + local OVEN_DEVICE_ID = 0x007B + local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) + if #oven_eps > 0 then + return true, require("matter-oven") + end + return false +end + +return is_oven_device diff --git a/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua index 9847f24191..6c31504a6a 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-oven/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -28,18 +18,10 @@ end local SUPPORTED_OVEN_MODES_MAP = "__supported_oven_modes_map_key_" -local OVEN_DEVICE_ID = 0x007B local COOK_SURFACE_DEVICE_TYPE_ID = 0x0077 local COOK_TOP_DEVICE_TYPE_ID = 0x0078 local TCC_DEVICE_TYPE_ID = 0x0071 -local function is_oven_device(opts, driver, device) - local oven_eps = common_utils.get_endpoints_for_dt(device, OVEN_DEVICE_ID) - if #oven_eps > 0 then - return true - end - return false -end -- Lifecycle Handlers -- local function device_added(driver, device) @@ -141,7 +123,7 @@ local matter_oven_handler = { [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = handle_temperature_setpoint } }, - can_handle = is_oven_device + can_handle = require("matter-oven.can_handle"), } return matter_oven_handler diff --git a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua new file mode 100644 index 0000000000..376b32cc2d --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_matter_refrigerator(opts, driver, device) + local REFRIGERATOR_DEVICE_TYPE_ID = 0x0070 + for _, ep in ipairs(device.endpoints) do + for _, dt in ipairs(ep.device_types) do + if dt.device_type_id == REFRIGERATOR_DEVICE_TYPE_ID then + return true, require("matter-refrigerator") + end + end + end + return false +end + +return is_matter_refrigerator diff --git a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua index ec57c61505..c48412e849 100644 --- a/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua +++ b/drivers/SmartThings/matter-appliance/src/matter-refrigerator/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -18,7 +8,6 @@ local common_utils = require "common-utils" local embedded_cluster_utils = require "embedded-cluster-utils" local version = require "version" -local REFRIGERATOR_DEVICE_TYPE_ID = 0x0070 local TEMPERATURE_CONTROLLED_CABINET_DEVICE_TYPE_ID = 0x0071 if version.api < 10 then @@ -29,17 +18,6 @@ end local SUPPORTED_REFRIGERATOR_TCC_MODES_MAP = "__supported_refrigerator_tcc_modes_map" -local function is_matter_refrigerator(opts, driver, device) - for _, ep in ipairs(device.endpoints) do - for _, dt in ipairs(ep.device_types) do - if dt.device_type_id == REFRIGERATOR_DEVICE_TYPE_ID then - return true - end - end - end - return false -end - -- Lifecycle Handlers -- local function device_added(driver, device) local cabinet_eps = {} @@ -182,7 +160,7 @@ local matter_refrigerator_handler = { [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = handle_temperature_setpoint } }, - can_handle = is_matter_refrigerator + can_handle = require("matter-refrigerator.can_handle"), } return matter_refrigerator_handler diff --git a/drivers/SmartThings/matter-appliance/src/sub_drivers.lua b/drivers/SmartThings/matter-appliance/src/sub_drivers.lua new file mode 100644 index 0000000000..e0ad824520 --- /dev/null +++ b/drivers/SmartThings/matter-appliance/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- 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("matter-cook-top"), + lazy_load_if_possible("matter-dishwasher"), + lazy_load_if_possible("matter-extractor-hood"), + lazy_load_if_possible("matter-laundry"), + lazy_load_if_possible("matter-microwave-oven"), + lazy_load_if_possible("matter-oven"), + lazy_load_if_possible("matter-refrigerator"), +} +return sub_drivers 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 9aa45f7951..4a65f8e882 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -194,4 +184,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua index c6923c3de4..f446d1149b 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) 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 91339b78ca..b8d18a3ab4 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 10e0de0f5e..9049b5f64e 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) 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 0334bf61cb..6f7ef3ee5c 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) 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 8f9a8c81d8..d8faa1e5ee 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(5) 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 3c2e1fbc51..624f1751f6 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua index 58c38e078c..76a23a0f44 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua index 3dc19750f3..0ac94856e3 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(6) From 377e001c26e560874f345c4b53958b6cbe173736 Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Tue, 16 Dec 2025 17:09:15 -0600 Subject: [PATCH 351/449] [Matter Camera] Add videoCapture2 action to embedded device config --- drivers/SmartThings/matter-switch/profiles/camera.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml index 2835810ae3..b7c8d8be34 100644 --- a/drivers/SmartThings/matter-switch/profiles/camera.yml +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -118,6 +118,11 @@ deviceConfig: - component: main capability: mechanicalPanTiltZoom version: 1 + automation: + actions: + - component: main + capability: videoCapture2 + version: 1 dpInfo: - os: ios dpUri: "storyboard://HMVSController/HMVSViewController" From b52b3c082f55739bf94e88877b4ba5fa2f5ee214 Mon Sep 17 00:00:00 2001 From: Jeron Aldaron Lau Date: Tue, 13 May 2025 17:20:22 -0500 Subject: [PATCH 352/449] Fix matter switch level behavior for small values --- .../src/switch_handlers/attribute_handlers.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 756a402de6..468faaaef4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -38,7 +38,10 @@ end function AttributeHandlers.level_control_current_level_handler(driver, device, ib, response) if ib.data.value ~= nil then - local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) + local level = ib.data.value + if level > 0 then + level = math.max(1, utils.round(level / 254.0 * 100)) + end device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) if type(device.register_native_capability_attr_handler) == "function" then device:register_native_capability_attr_handler("switchLevel", "level") @@ -535,4 +538,4 @@ function AttributeHandlers.percent_current_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.fanSpeedPercent.percent(ib.data.value)) end -return AttributeHandlers \ No newline at end of file +return AttributeHandlers From 5da560c7b729c99bde965282b4edb6788a5144f8 Mon Sep 17 00:00:00 2001 From: Jeron Aldaron Lau Date: Tue, 13 May 2025 17:22:17 -0500 Subject: [PATCH 353/449] Fix zigbee fan switch level behavior for small values --- .../src/switch_handlers/attribute_handlers.lua | 2 +- drivers/SmartThings/zigbee-fan/src/fan-light/init.lua | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) 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 468faaaef4..bb2d7f5aa8 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -40,7 +40,7 @@ function AttributeHandlers.level_control_current_level_handler(driver, device, i if ib.data.value ~= nil then local level = ib.data.value if level > 0 then - level = math.max(1, utils.round(level / 254.0 * 100)) + level = math.max(1, st_utils.round(level / 254.0 * 100)) end device:emit_event_for_endpoint(ib.endpoint_id, capabilities.switchLevel.level(level)) if type(device.register_native_capability_attr_handler) == "function" then diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua index 95329c1ddf..3ec72ddb8f 100644 --- a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua @@ -74,7 +74,11 @@ local function zb_fan_control_handler(driver, device, value, zb_rx) end local function zb_level_handler(driver, device, value, zb_rx) - local evt = capabilities.switchLevel.level(math.floor((value.value / 254.0 * 100) + 0.5)) + local level = value.value + if level > 0 then + level = math.max(1, math.floor((level / 254.0 * 100) + 0.5)) + end + local evt = capabilities.switchLevel.level(level) device:emit_component_event(device.profile.components.light, evt) end From 7b099ca887eeab53712c32acadc2f725862f5b5d Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Thu, 18 Dec 2025 10:09:37 -0600 Subject: [PATCH 354/449] Add motionSensor condition, and imageCapture action --- .../matter-switch/profiles/camera.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml index b7c8d8be34..7a46099cf6 100644 --- a/drivers/SmartThings/matter-switch/profiles/camera.yml +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -111,6 +111,16 @@ deviceConfig: capability: imageCapture version: 1 value: image.value + overlayIcons: + - iconUrl: "res://ic_camera_motion_detected" + visibleCondition: + component: main + capability: motionSensor + version: 1 + value: motion.value + valueType: string + operator: EQUALS + operand: "active" detailView: - component: main capability: webrtc @@ -118,11 +128,21 @@ deviceConfig: - component: main capability: mechanicalPanTiltZoom version: 1 + - component: main + capability: motionSensor + version: 1 automation: + conditions: + - component: main + capability: motionSensor + version: 1 actions: - component: main capability: videoCapture2 version: 1 + - component: main + capability: imageCapture + version: 1 dpInfo: - os: ios dpUri: "storyboard://HMVSController/HMVSViewController" From 1defafc592cc5117350125710ff3ee897d97c15a Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:29:44 -0600 Subject: [PATCH 355/449] Matter Thermostat: Support fanSpeedPercent (#2670) Support the fanSpeedPercent capability for thermostats based on the presence of the MULTI_SPEED feature. --- .../profiles/thermostat-modular.yml | 3 ++ ...st_matter_thermo_multiple_device_types.lua | 34 +++++++++++++++++++ .../thermostat_utils/device_configuration.lua | 9 ++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml index 1e7bd97dc7..7d3dd309f2 100644 --- a/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml +++ b/drivers/SmartThings/matter-thermostat/profiles/thermostat-modular.yml @@ -9,6 +9,9 @@ components: - id: fanMode version: 1 optional: true + - id: fanSpeedPercent + version: 1 + optional: true - id: fanOscillationMode version: 1 optional: true 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 a8acdc253e..2660f75b6e 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 @@ -3,6 +3,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local uint32 = require "st.matter.data_types.Uint32" @@ -196,6 +197,7 @@ local expected_metadata = { "main", { "relativeHumidityMeasurement", + "fanSpeedPercent", "fanMode", "fanOscillationMode", "thermostatHeatingSetpoint", @@ -220,6 +222,7 @@ local new_cluster_subscribe_list = { clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, clusters.FanControl.attributes.FanMode, clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.PercentCurrent, clusters.FanControl.attributes.RockSupport, -- These two attributes will be subscribed to following the profile clusters.FanControl.attributes.RockSetting, -- change since the fanOscillationMode capability will be enabled. } @@ -241,4 +244,35 @@ test.register_coroutine_test( { test_init = test_init_disorder_endpoints } ) +test.register_coroutine_test( + "PercentCurrent reports and setPercent commands should be handled correctly after profile change", + function() + test_thermostat_device_type_update_modular_profile(mock_device, expected_metadata, + get_subscribe_request(mock_device, new_cluster_subscribe_list)) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.FanControl.attributes.PercentCurrent:build_test_report_data(mock_device, 2, 10) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.fanSpeedPercent.percent(10)) + ) + + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "fanSpeedPercent", component = "main", command = "setPercent", args = { 50 } } + }) + + test.socket.matter:__expect_send({ + mock_device.id, + clusters.FanControl.attributes.PercentSetting:write(mock_device, 2, 50) + }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua index 102b6e8d3b..3ae5c76c7e 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/device_configuration.lua @@ -200,7 +200,14 @@ function DeviceConfiguration.match_modular_profile_thermostat(device) local wind_eps = device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.FanControlFeature.WIND}) if #fan_eps > 0 then - table.insert(main_component_capabilities, capabilities.fanMode.ID) + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.MULTI_SPEED}) > 0 then + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + if #device:get_endpoints(clusters.FanControl.ID, {feature_bitmap = clusters.FanControl.types.Feature.AUTO}) > 0 then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + else + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end end if #rock_eps > 0 then table.insert(main_component_capabilities, capabilities.fanOscillationMode.ID) From 9e159e3de19f6820f4c84e3d26aedfe6c89233dd Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:40:08 -0600 Subject: [PATCH 356/449] Matter Switch: Register native handlers for Hue and Saturation (#2687) * register native handlers for hue and saturation --- .../switch_handlers/attribute_handlers.lua | 20 ++++---- .../test/test_light_illuminance_motion.lua | 18 ++++++- .../src/test/test_matter_switch.lua | 48 ++++++++++++++++++- 3 files changed, 75 insertions(+), 11 deletions(-) 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 bb2d7f5aa8..1a36001d6c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -86,19 +86,23 @@ end -- [[ COLOR CONTROL CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.current_hue_handler(driver, device, ib, response) - if device:get_field(fields.COLOR_MODE) == fields.X_Y_COLOR_MODE or ib.data.value == nil then - return + if device:get_field(fields.COLOR_MODE) ~= fields.X_Y_COLOR_MODE and ib.data.value ~= nil then + local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("colorControl", "hue") end - local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) end function AttributeHandlers.current_saturation_handler(driver, device, ib, response) - if device:get_field(fields.COLOR_MODE) == fields.X_Y_COLOR_MODE or ib.data.value == nil then - return + if device:get_field(fields.COLOR_MODE) ~= fields.X_Y_COLOR_MODE and ib.data.value ~= nil then + local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + end + if type(device.register_native_capability_attr_handler) == "function" then + device:register_native_capability_attr_handler("colorControl", "saturation") end - local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) end function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, response) 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 dda93ccd01..b77558523d 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 @@ -355,6 +355,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -367,7 +375,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) 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 e38003ce5a..6cd59fa538 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -524,6 +524,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -536,7 +544,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) @@ -590,6 +606,14 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(100)) }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + }, { channel = "matter", direction = "receive", @@ -602,7 +626,15 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(100)) - } + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + }, } ) @@ -1083,6 +1115,12 @@ test.register_coroutine_test( "main", capabilities.colorControl.hue(100) ) ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } + ) test.socket.matter:__queue_receive( { mock_device.id, @@ -1094,6 +1132,12 @@ test.register_coroutine_test( "main", capabilities.colorControl.saturation(100) ) ) + test.socket.devices:__expect_send( + { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } + ) end, { test_init = test_init_x_y_color_mode } ) From 0e13a711c5beb9a9b8e4f5c2d0d1131597b2f8fc Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:33:17 -0600 Subject: [PATCH 357/449] Gate videoCapture2 based on PushAvStreamTransport (#2680) The VIDEO feature of CameraAvStreamManagement can be enabled on live-view only cameras, so the videoCapture2 capability should also be gated based on the presence of the PushAvStreamTransport clusters (presence of which also implies the presence of TLS clusters). --- .../sub_drivers/camera/camera_utils/device_configuration.lua | 4 +++- .../SmartThings/matter-switch/src/test/test_matter_camera.lua | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) 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 90c5ee3498..2ed821ddd2 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 @@ -62,7 +62,9 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr return clusters.CameraAvStreamManagement.are_features_supported(feature_bitmap, ep_cluster.feature_map) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.VIDEO) then - table.insert(main_component_capabilities, capabilities.videoCapture2.ID) + if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then + table.insert(main_component_capabilities, capabilities.videoCapture2.ID) + end table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) 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 b9804a2a76..97ffa5901e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -50,6 +50,10 @@ local mock_device = test.mock_device.build_test_matter_device({ clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, cluster_type = "SERVER" }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + }, { cluster_id = clusters.ZoneManagement.ID, feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | From bddd262ccd9c591b50a51c9826fcffab9a56c51f Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:33:34 -0600 Subject: [PATCH 358/449] Matter Camera: fix devices being marked online due to capability inits (#2688) The driverSwitched event can be triggered in a few different ways, but since it was pointed to the same handler as doConfigure, code that should only be ran for new devices was being run when this event was received by the driver. This includes `initialize_camera_capabilities`, which resulted in offline devices being marked online. This change adds separate handling from driverSwitched, specifically excluding this function as well as `create_child_devices`. --- .../matter-switch/src/sub_drivers/camera/init.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index 8244f4fd62..f17fb72d83 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -39,6 +39,13 @@ function CameraLifecycleHandlers.do_configure(driver, device) camera_cfg.initialize_camera_capabilities(device) end +function CameraLifecycleHandlers.driver_switched(driver, device) + camera_utils.update_camera_component_map(device) + if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then + camera_cfg.match_profile(device, false, false) + end +end + function CameraLifecycleHandlers.info_changed(driver, device, event, args) if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then camera_cfg.initialize_camera_capabilities(device) @@ -57,7 +64,7 @@ local camera_handler = { init = CameraLifecycleHandlers.device_init, infoChanged = CameraLifecycleHandlers.info_changed, doConfigure = CameraLifecycleHandlers.do_configure, - driverSwitched = CameraLifecycleHandlers.do_configure, + driverSwitched = CameraLifecycleHandlers.driver_switched, added = CameraLifecycleHandlers.added }, matter_handlers = { From 2d69ed871b82bc9c3e7e6fab73a30d56ee4e3a58 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:31 -0600 Subject: [PATCH 359/449] CHAD-17068: zigbee-humidity-sensor lazy load sub drivers --- .../src/aqara/can_handle.lua | 14 +++++++ .../src/aqara/fingerprints.lua | 8 ++++ .../zigbee-humidity-sensor/src/aqara/init.lua | 16 ++------ .../src/centralite-sensor/can_handle.lua | 14 +++++++ .../src/centralite-sensor/fingerprints.lua | 10 +++++ .../src/centralite-sensor/init.lua | 31 ++------------ .../src/configurations.lua | 2 +- .../frient-sensor/air-quality/can_handle.lua | 14 +++++++ .../air-quality/fingerprints.lua | 8 ++++ .../src/frient-sensor/air-quality/init.lua | 22 +++------- .../src/frient-sensor/can_handle.lua | 14 +++++++ .../src/frient-sensor/fingerprints.lua | 10 +++++ .../src/frient-sensor/init.lua | 40 +++---------------- .../src/frient-sensor/sub_drivers.lua | 9 +++++ .../src/heiman-sensor/can_handle.lua | 14 +++++++ .../src/heiman-sensor/fingerprints.lua | 11 +++++ .../src/heiman-sensor/init.lua | 32 ++------------- .../zigbee-humidity-sensor/src/init.lua | 25 ++---------- .../src/lazy_load_subdriver.lua | 15 +++++++ .../src/plaid-systems/can_handle.lua | 15 +++++++ .../src/plaid-systems/fingerprints.lua | 10 +++++ .../src/plaid-systems/init.lua | 32 ++------------- .../src/plant-link/can_handle.lua | 15 +++++++ .../src/plant-link/fingerprints.lua | 12 ++++++ .../src/plant-link/init.lua | 32 ++------------- .../src/sub_drivers.lua | 13 ++++++ .../src/test/test_aqara_sensor.lua | 2 +- .../src/test/test_centralite_sensor.lua | 15 +------ .../src/test/test_ewelink_sensor.lua | 15 +------ .../test/test_frient_air_quality_sensor.lua | 17 ++------ .../src/test/test_frient_sensor.lua | 17 ++------ .../src/test/test_heiman_sensor.lua | 15 +------ .../src/test/test_humidity_battery_sensor.lua | 15 +------ .../src/test/test_humidity_plaid_systems.lua | 15 +------ .../src/test/test_humidity_temperature.lua | 15 +------ .../test_humidity_temperature_battery.lua | 15 +------ .../test/test_humidity_temperature_sensor.lua | 15 +------ 37 files changed, 265 insertions(+), 329 deletions(-) create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..da7a97c64b --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..8e6df9f7d6 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sensor_ht.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua index 5256bf47e2..d3a8c7f89d 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -11,9 +14,6 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sensor_ht.agl02" } -} -- temperature: 0.5C, humidity: 2% local configuration = { @@ -43,14 +43,6 @@ local configuration = { } } -local is_aqara_products = function(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 battery_level_handler(driver, device, value, zb_rx) local voltage = value.value @@ -98,7 +90,7 @@ local aqara_humidity_handler = { init = device_init, added = added_handler }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_humidity_handler diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua new file mode 100644 index 0000000000..a36cc01536 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_centralite_sensor(opts, driver, device) + local FINGERPRINTS = require("centralite-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("centralite-sensor") + end + end + return false +end + +return can_handle_centralite_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua new file mode 100644 index 0000000000..df4f8a34a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3310-S" }, + { mfr = "CentraLite", model = "3310-G" }, + { mfr = "CentraLite", model = "3310" } +} + +return CENTRALITE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua index e1c11dd03b..11fedcfe24 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/centralite-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -23,20 +13,7 @@ local configurationMap = require "configurations" local HUMIDITY_CLUSTER_ID = 0xFC45 local HUMIDITY_MEASURE_ATTR_ID = 0x0000 -local CENTRALITE_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3310-S" }, - { mfr = "CentraLite", model = "3310-G" }, - { mfr = "CentraLite", model = "3310" } -} -local function can_handle_centralite_sensor(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_SENSOR_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_init(driver, device) device:remove_configured_attribute(clusters.RelativeHumidity.ID, clusters.RelativeHumidity.attributes.MeasuredValue.ID) @@ -89,7 +66,7 @@ local centralite_sensor = { } } }, - can_handle = can_handle_centralite_sensor + can_handle = require("centralite-sensor.can_handle"), } return centralite_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua index b040a4f73f..8bc35c8765 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/configurations.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua new file mode 100644 index 0000000000..f82f572b43 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient(opts, driver, device, ...) + local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = require ("frient-sensor.air-quality.fingerprints") + for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then + return true, require("frient-sensor.air-quality") + end + end + return false +end + +return can_handle_frient diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua new file mode 100644 index 0000000000..82c98c39fe --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" } +} + +return FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua index e9ca8e85cf..25ff436988 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/air-quality/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" @@ -11,20 +11,6 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local device_management = require "st.zigbee.device_management" local cluster_base = require "st.zigbee.cluster_base" local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local configurationMap = require "configurations" - -local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = { - { mfr = "frient A/S", model = "AQSZB-110", subdriver = "airquality" } -} - -local function can_handle_frient(opts, driver, device, ...) - for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and fingerprint.subdriver == "airquality" then - return true - end - end - return false -end local Frient_VOCMeasurement = { ID = 0xFC03, @@ -90,6 +76,7 @@ local function temperatureHandler(driver, device, attr_val, zb_rx) end local function device_init(driver, device) + local configurationMap = require "configurations" battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -106,6 +93,7 @@ local function device_added(driver, device) end local function do_refresh(driver, device) + local FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS = require "frient-sensor.air-quality.fingerprints" for _, fingerprint in ipairs(FRIENT_AIR_QUALITY_SENSOR_FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26)) @@ -153,7 +141,7 @@ local frient_airquality_sensor = { }, } }, - can_handle = can_handle_frient + can_handle = require("frient-sensor.air-quality.can_handle") } -return frient_airquality_sensor \ No newline at end of file +return frient_airquality_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua new file mode 100644 index 0000000000..060f58f76e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_frient_sensor(opts, driver, device) + local FINGERPRINTS = require("frient-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient-sensor") + end + end + return false +end + +return can_handle_frient_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua new file mode 100644 index 0000000000..f7969cafdd --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { + { mfr = "frient A/S", model = "HMSZB-110" }, + { mfr = "frient A/S", model = "HMSZB-120" }, + { mfr = "frient A/S", model = "AQSZB-110" } +} + +return FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua index 1356345ff2..d0d6190ad0 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/init.lua @@ -1,39 +1,13 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local battery_defaults = require "st.zigbee.defaults.battery_defaults" -local configurationMap = require "configurations" local zcl_clusters = require "st.zigbee.zcl.clusters" local HumidityMeasurement = zcl_clusters.RelativeHumidity local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement -local FRIENT_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { - { mfr = "frient A/S", model = "HMSZB-110" }, - { mfr = "frient A/S", model = "HMSZB-120" }, - { mfr = "frient A/S", model = "AQSZB-110" } -} - -local function can_handle_frient_sensor(opts, driver, device) - for _, fingerprint in ipairs(FRIENT_TEMP_HUMUDITY_SENSOR_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_init(driver, device) + local configurationMap = require "configurations" + local battery_defaults = require "st.zigbee.defaults.battery_defaults" battery_defaults.build_linear_voltage_init(2.3,3.0)(driver, device) local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then @@ -74,10 +48,8 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, - sub_drivers = { - require("frient-sensor/air-quality") - }, - can_handle = can_handle_frient_sensor + sub_drivers = require("frient-sensor.sub_drivers"), + can_handle = require("frient-sensor.can_handle"), } return frient_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua new file mode 100644 index 0000000000..cc45e1b394 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/frient-sensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("frient-sensor.air-quality") +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua new file mode 100644 index 0000000000..4e3aff5517 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_heiman_sensor(opts, driver, device) + local FINGERPRINTS = require("heiman-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("heiman-sensor") + end + end + return false +end + +return can_handle_heiman_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua new file mode 100644 index 0000000000..b0f9e1c7ab --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { + { mfr = "Heiman", model = "b467083cfc864f5e826459e5d8ea6079" }, + { mfr = "HEIMAN", model = "888a434f3cfc47f29ec4a3a03e9fc442" }, + { mfr = "HEIMAN", model = "HT-EM" }, + { mfr = "HEIMAN", model = "HT-EF-3.0" } +} + +return HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua index 10a60c8392..8be2857f70 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/heiman-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -20,21 +10,7 @@ local RelativeHumidity = clusters.RelativeHumidity local TemperatureMeasurement = clusters.TemperatureMeasurement local PowerConfiguration = clusters.PowerConfiguration -local HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS = { - { mfr = "Heiman", model = "b467083cfc864f5e826459e5d8ea6079" }, - { mfr = "HEIMAN", model = "888a434f3cfc47f29ec4a3a03e9fc442" }, - { mfr = "HEIMAN", model = "HT-EM" }, - { mfr = "HEIMAN", model = "HT-EF-3.0" } -} -local function can_handle_heiman_sensor(opts, driver, device) - for _, fingerprint in ipairs(HEIMAN_TEMP_HUMUDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_refresh(driver, device) device:send(RelativeHumidity.attributes.MeasuredValue:read(device):to_endpoint(0x02)) @@ -59,7 +35,7 @@ local heiman_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = can_handle_heiman_sensor + can_handle = require("heiman-sensor.can_handle"), } return heiman_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua index 83a14c70dc..9ca7cd734d 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -78,14 +68,7 @@ local zigbee_humidity_driver = { init = device_init, added = added_handler, }, - sub_drivers = { - require("aqara"), - require("plant-link"), - require("plaid-systems"), - require("centralite-sensor"), - require("heiman-sensor"), - require("frient-sensor") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua new file mode 100644 index 0000000000..a11116318e --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plaid_systems_humidity_sensor = function(opts, driver, device) + local FINGERPRINTS = require("plaid-systems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("plaid-systems") + end + end + + return false +end + +return is_zigbee_plaid_systems_humidity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua new file mode 100644 index 0000000000..fbacec9269 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-01" }, + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP1" }, + { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP3" }, +} + +return ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua index 2233aab2a3..093007adf9 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plaid-systems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" @@ -21,21 +11,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local RelativeHumidity = zcl_clusters.RelativeHumidity local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement -local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-01" }, - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP1" }, - { mfr = "PLAID SYSTEMS", model = "PS-SPRZMS-SLP3" }, -} -local is_zigbee_plaid_systems_humidity_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local battery_mains_voltage_attr_handler = function(driver, device, value, zb_rx) local min = 2500 @@ -87,7 +63,7 @@ local plaid_systems_humdity_sensor = { } } }, - can_handle = is_zigbee_plaid_systems_humidity_sensor + can_handle = require("plaid-systems.can_handle"), } return plaid_systems_humdity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua new file mode 100644 index 0000000000..9dcbc3e407 --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_plant_link_humidity_sensor = function(opts, driver, device) + local FINGERPRINTS = require("plant-link.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and device:supports_server_cluster(fingerprint.cluster_id) then + return true, require("plant-link") + end + end + + return false +end + +return is_zigbee_plant_link_humidity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua new file mode 100644 index 0000000000..3b5e5f648d --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/fingerprints.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER = 0xFC08 + +local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { + { mfr = "", model = "", cluster_id = PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER }, + { mfr = "", model = "", cluster_id = zcl_clusters.ElectricalMeasurement.ID } +} + +return ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua index 0a2d7fb225..07ab4a095e 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/plant-link/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -18,12 +8,7 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local RelativeHumidity = zcl_clusters.RelativeHumidity local utils = require "st.utils" -local PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER = 0xFC08 -local ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS = { - { mfr = "", model = "", cluster_id = PLANT_LINK_MANUFACTURER_SPECIFIC_CLUSTER }, - { mfr = "", model = "", cluster_id = zcl_clusters.ElectricalMeasurement.ID } -} local humidity_value_attr_handler = function(driver, device, value, zb_rx) -- adc reading of 0x1ec0 produces a plant fuel level near 0 @@ -44,15 +29,6 @@ local battery_mains_voltage_attr_handler = function(driver, device, value, zb_rx device:emit_event(capabilities.battery.battery(percent)) end -local is_zigbee_plant_link_humidity_sensor = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_HUMIDITY_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model and device:supports_server_cluster(fingerprint.cluster_id) then - return true - end - end - - return false -end local plant_link_humdity_sensor = { NAME = "PlantLink Soil Moisture Sensor", @@ -70,7 +46,7 @@ local plant_link_humdity_sensor = { } } }, - can_handle = is_zigbee_plant_link_humidity_sensor + can_handle = require("plant-link.can_handle"), } return plant_link_humdity_sensor diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..6f49a5f18c --- /dev/null +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/sub_drivers.lua @@ -0,0 +1,13 @@ +-- 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("aqara"), + lazy_load_if_possible("plant-link"), + lazy_load_if_possible("plaid-systems"), + lazy_load_if_possible("centralite-sensor"), + lazy_load_if_possible("heiman-sensor"), + lazy_load_if_possible("frient-sensor"), +} +return sub_drivers 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 8f414975a1..497395ac55 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 @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. 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 626d704f2a..c9b469376c 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 17d9afd2a5..35f557e850 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 607a8aa19e..e985dafce5 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -302,4 +291,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 c2723d0edd..5b9ef77634 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" @@ -231,4 +220,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 3dd7f5c719..773bd3e9e4 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 62f6421fa7..f914f0a898 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 d9745bb681..6bca6c4e1a 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local base64 = require "st.base64" local capabilities = require "st.capabilities" 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 f3e7831b1f..6de1dceb18 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 9afd1c3a1d..341cd4bc20 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 d4f3c09a05..4282db0ffd 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From 50e0092e2267a5e94010e6bb836c20631d6140b4 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:22 -0600 Subject: [PATCH 360/449] CHAD-17098: zwave-window-treatment lazy loading of sub-drivers --- .../src/aeotec-nano-shutter/can_handle.lua | 14 +++++++ .../src/aeotec-nano-shutter/fingerprints.lua | 11 ++++++ .../src/aeotec-nano-shutter/init.lua | 32 ++-------------- .../iblinds-window-treatment/can_handle.lua | 14 +++++++ .../iblinds-window-treatment/fingerprints.lua | 10 +++++ .../src/iblinds-window-treatment/init.lua | 35 +++--------------- .../iblinds-window-treatment/sub_drivers.lua | 8 ++++ .../v3/can_handle.lua | 19 ++++++++++ .../v3/fingerprints.lua | 9 +++++ .../{v3.lua => v3/init.lua} | 16 ++------ .../zwave-window-treatment/src/init.lua | 23 ++---------- .../src/lazy_load_subdriver.lua | 18 +++++++++ .../src/preferences.lua | 16 ++------ .../can_handle.lua | 14 +++++++ .../fingerprints.lua | 9 +++++ .../src/springs-window-fashion-shade/init.lua | 30 ++------------- .../src/sub_drivers.lua | 11 ++++++ .../src/test/test_fibaro_roller_shutter.lua | 16 ++------ .../src/test/test_qubino_flush_shutter.lua | 16 ++------ .../test/test_zwave_aeotec_nano_shutter.lua | 16 ++------ .../test_zwave_iblinds_window_treatment.lua | 16 ++------ .../test_zwave_springs_window_treatment.lua | 16 ++------ .../src/test/test_zwave_window_treatment.lua | 16 ++------ .../window-treatment-venetian/can_handle.lua | 14 +++++++ .../fibaro-roller-shutter/can_handle.lua | 14 +++++++ .../fibaro-roller-shutter/fingerprints.lua | 8 ++++ .../fibaro-roller-shutter/init.lua | 29 ++------------- .../fingerprints.lua | 10 +++++ .../src/window-treatment-venetian/init.lua | 37 +++---------------- .../qubino-flush-shutter/can_handle.lua | 14 +++++++ .../qubino-flush-shutter/fingerprints.lua | 9 +++++ .../qubino-flush-shutter/init.lua | 30 ++------------- .../window-treatment-venetian/sub_drivers.lua | 9 +++++ .../src/window_preset_defaults.lua | 18 ++------- 34 files changed, 274 insertions(+), 303 deletions(-) create mode 100644 drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua rename drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/{v3.lua => v3/init.lua} (84%) create mode 100644 drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua new file mode 100644 index 0000000000..7caf966d19 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_nano_shutter(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-nano-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("aeotec-nano-shutter") + end + end + return false +end + +return can_handle_aeotec_nano_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua new file mode 100644 index 0000000000..dcbcf2f872 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_NANO_SHUTTER_FINGERPRINTS = { + {mfr = 0x0086, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU + {mfr = 0x0086, prod = 0x0103, model = 0x008D}, -- Aeotec nano shutter US + {mfr = 0x0371, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU + {mfr = 0x0371, prod = 0x0103, model = 0x008D} -- Aeotec nano shutter US +} + +return AEOTEC_NANO_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua index 6ca6fd7837..82977a7cfb 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/aeotec-nano-shutter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -28,26 +18,12 @@ local SET_BUTTON_TO_CLOSE = "close" local SET_BUTTON_TO_PAUSE = "pause" local SHADE_STATE = "shade_state" -local AEOTEC_NANO_SHUTTER_FINGERPRINTS = { - {mfr = 0x0086, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU - {mfr = 0x0086, prod = 0x0103, model = 0x008D}, -- Aeotec nano shutter US - {mfr = 0x0371, prod = 0x0003, model = 0x008D}, -- Aeotec nano shutter EU - {mfr = 0x0371, prod = 0x0103, model = 0x008D} -- Aeotec nano shutter US -} --- Determine whether the passed device is proper --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is proper, else false -local function can_handle_aeotec_nano_shutter(opts, driver, device, ...) - for _, fingerprint in ipairs(AEOTEC_NANO_SHUTTER_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end --- Default handler for basic reports for the devices --- @@ -140,7 +116,7 @@ local aeotec_nano_shutter = { added = added_handler }, NAME = "Aeotec nano shutter", - can_handle = can_handle_aeotec_nano_shutter + can_handle = require("aeotec-nano-shutter.can_handle"), } return aeotec_nano_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua new file mode 100644 index 0000000000..881ebd36a3 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_iblinds_window_treatment(opts, driver, device, ...) + local FINGERPRINTS = require("iblinds-window-treatment.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("iblinds-window-treatment") + end + end + return false +end + +return can_handle_iblinds_window_treatment diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua new file mode 100644 index 0000000000..d4f652f8b9 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS = { + {mfr = 0x0287, prod = 0x0003, model = 0x000D}, -- iBlinds Window Treatment v1 / v2 + {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 + {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3.1 +} + +return IBLINDS_WINDOW_TREATMENT_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua index 690e50d694..9c3d2d9970 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/init.lua @@ -1,41 +1,18 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ version=3 }) local window_preset_defaults = require "window_preset_defaults" -local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS = { - {mfr = 0x0287, prod = 0x0003, model = 0x000D}, -- iBlinds Window Treatment v1 / v2 - {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 - {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3.1 -} --- Determine whether the passed device is iblinds window treatment --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is iblinds window treatment, else false -local function can_handle_iblinds_window_treatment(opts, driver, device, ...) - for _, fingerprint in ipairs(IBLINDS_WINDOW_TREATMENT_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local capability_handlers = {} @@ -91,11 +68,9 @@ local iblinds_window_treatment = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = capability_handlers.preset_position } }, - sub_drivers = { - require("iblinds-window-treatment.v3") - }, + sub_drivers = require("iblinds-window-treatment.sub_drivers"), NAME = "iBlinds window treatment", - can_handle = can_handle_iblinds_window_treatment + can_handle = require("iblinds-window-treatment.can_handle"), } return iblinds_window_treatment diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua new file mode 100644 index 0000000000..187ef0ee04 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("iblinds-window-treatment.v3") +} diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua new file mode 100644 index 0000000000..ababe18ac6 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/can_handle.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is iblinds window treatment v3 +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @return boolean true if the device is iblinds window treatment, else false +local function can_handle_iblinds_window_treatment_v3(opts, driver, device, ...) + local FINGERPRINTS = require("iblinds-window-treatment.v3.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("iblinds-window-treatment.v3") + end + end + return false +end + +return can_handle_iblinds_window_treatment_v3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua new file mode 100644 index 0000000000..ee256ea18a --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 = { + {mfr = 0x0287, prod = 0x0004, model = 0x0071}, -- iBlinds Window Treatment v3 + {mfr = 0x0287, prod = 0x0004, model = 0x0072} -- iBlinds Window Treatment v3 +} + +return IBLINDS_WINDOW_TREATMENT_FINGERPRINTS_V3 diff --git a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua similarity index 84% rename from drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua rename to drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua index 9e0a38775d..c0bb8bc363 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/iblinds-window-treatment/v3/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.SwitchMultilevel diff --git a/drivers/SmartThings/zwave-window-treatment/src/init.lua b/drivers/SmartThings/zwave-window-treatment/src/init.lua index aaaf5c7889..b2597d9f61 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.defaults @@ -84,12 +74,7 @@ local driver_template = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_preset_defaults.window_shade_preset_cmd, } }, - sub_drivers = { - require("springs-window-fashion-shade"), - require("iblinds-window-treatment"), - require("window-treatment-venetian"), - require("aeotec-nano-shutter") - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-window-treatment/src/preferences.lua b/drivers/SmartThings/zwave-window-treatment/src/preferences.lua index e8854c0bc0..54bad4c0ec 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/preferences.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { QUBINO = { diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua new file mode 100644 index 0000000000..12d85394cf --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_springs_window_fashion_shade(opts, driver, device, ...) + local FINGERPRINTS = require("springs-window-fashion-shade.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("springs-window-fashion-shade") + end + end + return false +end + +return can_handle_springs_window_fashion_shade diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua new file mode 100644 index 0000000000..7730b633e8 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SPRINGS_WINDOW_FINGERPRINTS = { + {mfr = 0x026E, prod = 0x4353, model = 0x5A31}, -- Springs Window Shade + {mfr = 0x026E, prod = 0x5253, model = 0x5A31}, -- Springs Roller Shade +} + +return SPRINGS_WINDOW_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua index e501eecdff..2f4d3c38e6 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/springs-window-fashion-shade/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.constants @@ -18,24 +8,12 @@ local constants = require "st.zwave.constants" --- @type st.zwave.CommandClass.SwitchMultilevel local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({version=4}) -local SPRINGS_WINDOW_FINGERPRINTS = { - {mfr = 0x026E, prod = 0x4353, model = 0x5A31}, -- Springs Window Shade - {mfr = 0x026E, prod = 0x5253, model = 0x5A31}, -- Springs Roller Shade -} --- Determine whether the passed device is springs window fashion shade --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is springs window fashion shade, else false -local function can_handle_springs_window_fashion_shade(opts, driver, device, ...) - for _, fingerprint in ipairs(SPRINGS_WINDOW_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function init_handler(self, device) -- This device has a preset position set in hardware, so we need to override the base driver @@ -77,7 +55,7 @@ local springs_window_fashion_shade = { } }, NAME = "Springs window fashion shade", - can_handle = can_handle_springs_window_fashion_shade, + can_handle = require("springs-window-fashion-shade.can_handle"), } return springs_window_fashion_shade diff --git a/drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/sub_drivers.lua new file mode 100644 index 0000000000..6db8853e1a --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/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("springs-window-fashion-shade"), + lazy_load_if_possible("iblinds-window-treatment"), + lazy_load_if_possible("window-treatment-venetian"), + lazy_load_if_possible("aeotec-nano-shutter"), +} +return sub_drivers 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 9c879806f6..c601ba630f 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 5431ed1e70..edaffb4214 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 c11b578b07..089fc29bac 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index f53e540fc4..e2134b1163 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua index be3b3a6405..843b26e407 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_springs_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local constants = require "st.zwave.constants" diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index 534f6c56f7..fa6fd809e7 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua new file mode 100644 index 0000000000..f2eb62ad27 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_window_treatment_venetian(opts, driver, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian") + end + end + return false +end + +return can_handle_window_treatment_venetian diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua new file mode 100644 index 0000000000..cc9ca62b1c --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_roller_shutter(opts, driver, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.fibaro-roller-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian.fibaro-roller-shutter") + end + end + return false +end + +return can_handle_fibaro_roller_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua new file mode 100644 index 0000000000..925685157f --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_ROLLER_SHUTTER_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter +} + +return FIBARO_ROLLER_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua index dbdde5b87c..5581e3a5af 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fibaro-roller-shutter/init.lua @@ -1,25 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = (require "st.zwave.CommandClass") --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({version=1}) -local FIBARO_ROLLER_SHUTTER_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter -} -- configuration parameters local CALIBRATION_CONFIGURATION = 150 @@ -33,14 +20,6 @@ local CLB_NOT_STARTED = "not_started" local CLB_DONE = "done" local CLB_PENDING = "pending" -local function can_handle_fibaro_roller_shutter(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_ROLLER_SHUTTER_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function configuration_report(driver, device, cmd) local parameter_number = cmd.args.parameter_number @@ -79,7 +58,7 @@ local fibaro_roller_shutter = { } }, NAME = "fibaro roller shutter", - can_handle = can_handle_fibaro_roller_shutter, + can_handle = require("window-treatment-venetian.fibaro-roller-shutter.can_handle"), lifecycle_handlers = { add = device_added } diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua new file mode 100644 index 0000000000..91dc96c5b8 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WINDOW_TREATMENT_VENETIAN_FINGERPRINTS = { + {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter + {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC + {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC +} + +return WINDOW_TREATMENT_VENETIAN_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua index 2ff27250a8..c201ed32eb 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cc = (require "st.zwave.CommandClass") local Basic = (require "st.zwave.CommandClass.Basic")({ version=1 }) @@ -19,20 +10,7 @@ local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ver local WindowShadeDefaults = require "st.zwave.defaults.windowShade" local WindowShadeLevelDefaults = require "st.zwave.defaults.windowShadeLevel" -local WINDOW_TREATMENT_VENETIAN_FINGERPRINTS = { - {mfr = 0x010F, prod = 0x1D01, model = 0x1000}, -- Fibaro Walli Roller Shutter - {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC - {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC -} -local function can_handle_window_treatment_venetian(opts, driver, device, ...) - for _, fingerprint in ipairs(WINDOW_TREATMENT_VENETIAN_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function shade_event_handler(self, device, cmd) WindowShadeDefaults.zwave_handlers[cc.SWITCH_MULTILEVEL][SwitchMultilevel.REPORT](self, device, cmd) @@ -70,14 +48,11 @@ local window_treatment_venetian = { [SwitchMultilevel.REPORT] = shade_event_handler } }, - can_handle = can_handle_window_treatment_venetian, + can_handle = require("window-treatment-venetian.can_handle"), lifecycle_handlers = { init = map_components }, - sub_drivers = { - require("window-treatment-venetian/fibaro-roller-shutter"), - require("window-treatment-venetian/qubino-flush-shutter") - } + sub_drivers = require("window-treatment-venetian.sub_drivers"), } return window_treatment_venetian diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua new file mode 100644 index 0000000000..e601800084 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_qubino_flush_shutter(opts, self, device, ...) + local FINGERPRINTS = require("window-treatment-venetian.qubino-flush-shutter.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then + return true, require("window-treatment-venetian.qubino-flush-shutter") + end + end + return false +end + +return can_handle_qubino_flush_shutter diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua new file mode 100644 index 0000000000..098879f313 --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local QUBINO_FLUSH_SHUTTER_FINGERPRINTS = { + {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC + {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC +} + +return QUBINO_FLUSH_SHUTTER_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua index fafc6d5026..56ba50f516 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/qubino-flush-shutter/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -41,19 +31,7 @@ local SHADE_TARGET = "shade_target" local ENERGY_UNIT_KWH = "kWh" local POWER_UNIT_WATT = "W" -local QUBINO_FLUSH_SHUTTER_FINGERPRINTS = { - {mfr = 0x0159, prod = 0x0003, model = 0x0052}, -- Qubino Flush Shutter AC - {mfr = 0x0159, prod = 0x0003, model = 0x0053}, -- Qubino Flush Shutter DC -} -local function can_handle_qubino_flush_shutter(opts, self, device, ...) - for _, fingerprint in ipairs(QUBINO_FLUSH_SHUTTER_FINGERPRINTS) do - if device:id_match( fingerprint.mfr, fingerprint.prod, fingerprint.model) then - return true - end - end - return false -end local function configuration_report(self, device, cmd) local parameter_number = cmd.args.parameter_number @@ -190,7 +168,7 @@ local qubino_flush_shutter = { [capabilities.windowShade.commands.close.NAME] = close }, }, - can_handle = can_handle_qubino_flush_shutter, + can_handle = require("window-treatment-venetian.qubino-flush-shutter.can_handle"), lifecycle_handlers = { added = device_added, infoChanged = info_changed diff --git a/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua new file mode 100644 index 0000000000..41600cffef --- /dev/null +++ b/drivers/SmartThings/zwave-window-treatment/src/window-treatment-venetian/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("window-treatment-venetian/fibaro-roller-shutter"), + lazy_load_if_possible("window-treatment-venetian/qubino-flush-shutter"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua index 5046940dea..3d5056abc5 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/window_preset_defaults.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- These were added to scripting engine, but this file is to make sure drivers @@ -62,4 +52,4 @@ defaults.window_shade_preset_cmd = function(driver, device, command) device.thread:call_with_delay(constants.MIN_DIMMING_GET_STATUS_DELAY, query_device) end -return defaults \ No newline at end of file +return defaults From 1251d573ea0049bee5c757bd67213add78976b96 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Thu, 8 Jan 2026 15:41:20 -0600 Subject: [PATCH 361/449] fixup: Fixed utf-8 errors in zigbee-motion-sensor tests --- .../src/test/test_ikea_motion.lua | 4 ++-- .../src/test/test_smartsense_motion_sensor.lua | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) 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 0173ac431d..12e0038c6c 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 @@ -123,7 +123,7 @@ test.register_coroutine_test( test.register_coroutine_test( "ZDO Message handler and adding hub to group", function() - local binding_table = mgmt_bind_response.BindingTableListRecord("j^", 0x01, 0x0006, 0x01, 0xB9F2) + local binding_table = mgmt_bind_response.BindingTableListRecord("\x6A\x9D\xC0\xFE\xFF\x5E\xCF\xD0", 0x01, 0x0006, 0x01, 0xB9F2) local response = mgmt_bind_response.MgmtBindResponse({ status = 0x00, total_binding_table_entry_count = 0x01, @@ -144,7 +144,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Request all binding table entries and fall back to group 0x0000", function() - local binding_table_long = mgmt_bind_response.BindingTableListRecord("j^", 0x01, 0x0006, 0x03, "DEADBEEF", 0x01) + local binding_table_long = mgmt_bind_response.BindingTableListRecord("\x6A\x9D\xC0\xFE\xFF\x5E\xCF\xD0", 0x01, 0x0006, 0x03, "DEADBEEF", 0x01) local response = mgmt_bind_response.MgmtBindResponse({ status = 0x00, total_binding_table_entry_count = 0x02, 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 007c8448bf..f96322467f 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 @@ -61,7 +61,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "|") + build_motion_status_message(mock_device, "\x7C") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) @@ -75,7 +75,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "~") + build_motion_status_message(mock_device, "\x7E") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) @@ -89,7 +89,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "X") + build_motion_status_message(mock_device, "\x58") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(70))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) @@ -103,7 +103,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "Z") + build_motion_status_message(mock_device, "\x5A") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(70))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) @@ -117,7 +117,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "") + build_motion_status_message(mock_device, "\xBD") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.lqi(50))) @@ -130,7 +130,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "") + build_motion_status_message(mock_device, "\xBF") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.lqi(50))) @@ -143,7 +143,7 @@ test.register_coroutine_test( function() test.socket.zigbee:__queue_receive({ mock_device.id, - build_motion_status_message(mock_device, "0") + build_motion_status_message(mock_device, "\x30") }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) From b8b70686af6bfb80cc351a31c5e6d59653a88d72 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:56:06 -0600 Subject: [PATCH 362/449] Exclude off from fanMode capability for Thermostats (#2673) Exclude the off mode from the supportedFanModes attribute of the fanMode capability for Thermostats. --- .../attribute_handlers.lua | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) 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 78f5ee3bf2..fd5dca06a4 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -48,7 +48,11 @@ function AttributeHandlers.system_mode_handler(driver, device, ib, response) return end - local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} + local supported_modes = device:get_latest_state( + device:endpoint_to_component(ib.endpoint_id), + capabilities.thermostatMode.ID, + capabilities.thermostatMode.supportedThermostatModes.NAME + ) or {} -- check that the given mode was in the supported modes list if thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) @@ -325,19 +329,19 @@ function AttributeHandlers.fan_mode_handler(driver, device, ib, response) end function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) - local supportedFanModes, supported_fan_modes_attribute + local supported_fan_modes, supported_fan_modes_attribute if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supportedFanModes = { "off", "low", "medium", "high" } + supported_fan_modes = { "off", "low", "medium", "high" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supportedFanModes = { "off", "low", "high" } + supported_fan_modes = { "off", "low", "high" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supportedFanModes = { "off", "low", "medium", "high", "auto" } + supported_fan_modes = { "off", "low", "medium", "high", "auto" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supportedFanModes = { "off", "low", "high", "auto" } + supported_fan_modes = { "off", "low", "high", "auto" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then - supportedFanModes = { "off", "high", "auto" } + supported_fan_modes = { "off", "high", "auto" } else - supportedFanModes = { "off", "high" } + supported_fan_modes = { "off", "high" } end if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then @@ -349,16 +353,22 @@ function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, respons -- Our thermostat fan mode control is not granular enough to handle all of the supported modes if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supportedFanModes = { "auto", "on" } + supported_fan_modes = { "auto", "on" } else - supportedFanModes = { "on" } + supported_fan_modes = { "on" } end else supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes end - local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) - device:emit_event_for_endpoint(ib.endpoint_id, event) + -- remove 'off' as a supported fan mode for thermostat device types + if thermostat_utils.get_device_type(device) == fields.THERMOSTAT_DEVICE_TYPE_ID and + device:supports_capability_by_id(capabilities.fanMode.ID) then + -- per the definitions set above, the first index always contains "off" + table.remove(supported_fan_modes, 1) + end + + device:emit_event_for_endpoint(ib.endpoint_id, supported_fan_modes_attribute(supported_fan_modes, {visibility = {displayed = false}})) end function AttributeHandlers.percent_current_handler(driver, device, ib, response) From 992bc3f1db4a2e49772be7e32a427f47d9b203db Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 8 Jan 2026 16:04:04 -0600 Subject: [PATCH 363/449] update floor to round in level cap handler --- .../src/switch_handlers/capability_handlers.lua | 2 +- .../matter-switch/src/test/test_electrical_sensor_set.lua | 3 ++- .../matter-switch/src/test/test_electrical_sensor_tree.lua | 3 ++- .../matter-switch/src/test/test_light_illuminance_motion.lua | 3 ++- .../matter-switch/src/test/test_matter_switch.lua | 5 +++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index 2c5241de3a..f96686b73a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -42,7 +42,7 @@ function CapabilityHandlers.handle_switch_set_level(driver, device, cmd) device:register_native_capability_cmd_handler(cmd.capability, cmd.command) end local endpoint_id = device:component_to_endpoint(cmd.component) - local level = math.floor(cmd.args.level/100.0 * 254) + local level = st_utils.round(cmd.args.level/100.0 * 254) device:send(clusters.LevelControl.server.commands.MoveToLevelWithOnOff(device, endpoint_id, level, cmd.args.rate, 0, 0)) end 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 412c773540..8e3ad613da 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 @@ -7,6 +7,7 @@ local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local version = require "version" +local st_utils = require "st.utils" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" @@ -678,7 +679,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { 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 a16d7372b5..b548bb819b 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 @@ -7,6 +7,7 @@ local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local version = require "version" +local st_utils = require "st.utils" if version.api < 11 then clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" @@ -334,7 +335,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 2, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { 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 b77558523d..24987079fc 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 @@ -4,6 +4,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 @@ -221,7 +222,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { 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 6cd59fa538..25bc0fdfaa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -4,6 +4,7 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 @@ -343,7 +344,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, math.floor(20/100.0 * 254), 20, 0 ,0) + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(mock_device, 1, st_utils.round(20/100.0 * 254), 20, 0 ,0) } }, { @@ -414,7 +415,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.switchLevel.level(math.floor((50 / 254.0 * 100) + 0.5))) + message = mock_device:generate_test_message("main", capabilities.switchLevel.level(st_utils.round((50 / 254.0 * 100) + 0.5))) }, { channel = "devices", From c795f829a9109831c261ee8ca4ac8dd189bf3366 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:40:59 -0600 Subject: [PATCH 364/449] Matter Switch: Attempt re-profiling on device software updates (#2466) * re-profile device if a matter_version update occurs --- .../SmartThings/matter-switch/src/init.lua | 44 +++++-------- .../camera_utils/device_configuration.lua | 2 +- .../src/sub_drivers/camera/init.lua | 2 +- .../src/switch_utils/device_configuration.lua | 32 ++++++---- .../matter-switch/src/switch_utils/utils.lua | 23 +++++++ .../test/test_aqara_climate_sensor_w100.lua | 1 + .../src/test/test_aqara_light_switch_h2.lua | 1 + .../src/test/test_matter_button.lua | 1 + .../src/test/test_matter_camera.lua | 1 + .../src/test/test_matter_multi_button.lua | 4 +- .../test/test_matter_multi_button_motion.lua | 3 +- .../test_matter_multi_button_switch_mcd.lua | 64 +++++++++++-------- .../test/test_matter_switch_device_types.lua | 4 ++ .../test_multi_switch_parent_child_lights.lua | 14 ++++ 14 files changed, 124 insertions(+), 72 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 5436d57baf..c2052dfe50 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -60,16 +60,21 @@ end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if #button_eps > 0 and device.network_type == device_lib.NETWORK_TYPE_MATTER then - button_cfg.configure_buttons(device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + device:subscribe() + button_cfg.configure_buttons(device, + device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + ) + elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + switch_utils.update_subscriptions(device:get_parent_device()) -- parent device required to scan through EPs and update subscriptions end end -end -function SwitchLifecycleHandlers.device_removed(driver, device) - device.log.info("device removed") + if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + if device.matter_version.software ~= args.old_st_store.matter_version.software then + device_cfg.match_profile(driver, device) + end + end end function SwitchLifecycleHandlers.device_init(driver, device) @@ -80,26 +85,7 @@ function SwitchLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end - local default_endpoint_id = switch_utils.find_default_endpoint(device) - -- ensure subscription to all endpoint attributes- including those mapped to child devices - for _, ep in ipairs(device.endpoints) do - if ep.endpoint_id ~= default_endpoint_id then - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do - if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and - attr ~= clusters.PowerSource.attributes.BatPercentRemaining and - attr ~= clusters.PowerSource.attributes.BatChargeLevel then - device:add_subscribed_event(attr) - else - device:add_subscribed_attribute(attr) - end - end - end - end - device:subscribe() + switch_utils.update_subscriptions(device) -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. @@ -110,6 +96,10 @@ function SwitchLifecycleHandlers.device_init(driver, device) end end +function SwitchLifecycleHandlers.device_removed(driver, device) + device.log.info("device removed") +end + local matter_driver_template = { lifecycle_handlers = { added = SwitchLifecycleHandlers.device_added, 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 2ed821ddd2..6626729c78 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 @@ -122,7 +122,7 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr if #doorbell_endpoints > 0 then table.insert(doorbell_component_capabilities, capabilities.button.ID) CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) - button_cfg.configure_buttons(device) + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) end if status_light_enabled_present then table.insert(status_led_component_capabilities, capabilities.switch.ID) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index f17fb72d83..e8d5aad8fa 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -50,7 +50,7 @@ function CameraLifecycleHandlers.info_changed(driver, device, event, args) if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then camera_cfg.initialize_camera_capabilities(device) if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then - button_cfg.configure_buttons(device) + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) end device:subscribe() 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 750c9eb50c..9f702ffd75 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -43,7 +43,7 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on return generic_profile or "switch-binary" end -function SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) +function SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == default_endpoint_id then -- no children will be created return end @@ -53,16 +53,21 @@ function SwitchDeviceConfiguration.create_child_devices(driver, device, server_o if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint local label_and_name = string.format("%s %d", device.label, device_num) local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, ep_id, true) - driver:try_create_device( - { + 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 + driver:try_create_device({ type = "EDGE_CHILD", label = label_and_name, profile = child_profile, parent_device_id = device.id, parent_assigned_child_key = string.format("%d", ep_id), vendor_provided_label = label_and_name - } - ) + }) + else + existing_child_device:try_update_metadata({ + profile = child_profile + }) + end end end @@ -121,13 +126,12 @@ function ButtonDeviceConfiguration.update_button_component_map(device, default_e end -function ButtonDeviceConfiguration.configure_buttons(device) - local ms_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) +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}) local msm_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS}) - for _, ep in ipairs(ms_eps) do + for _, ep in ipairs(momentary_switch_ep_ids or {}) do if device.profile.components[switch_utils.endpoint_to_component(device, ep)] then device.log.info_with({hub_logs=true}, string.format("Configuring Supported Values for generic switch endpoint %d", ep)) local supportedButtonValues_event @@ -184,7 +188,7 @@ function DeviceConfiguration.match_profile(driver, device) local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then - SwitchDeviceConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) + SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) end if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then @@ -206,12 +210,12 @@ function DeviceConfiguration.match_profile(driver, device) end -- initialize the main device card with buttons if applicable - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #button_eps) then - ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #button_eps) + local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then + ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids) -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. - ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) - ButtonDeviceConfiguration.configure_buttons(device) + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momemtary_switch_ep_ids) + ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids) return end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index b258688234..7bbb5c55e8 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -427,4 +427,27 @@ function utils.lazy_load_if_possible(sub_driver_name) end end +function utils.update_subscriptions(device) + local default_endpoint_id = utils.find_default_endpoint(device) + -- ensure subscription to all endpoint attributes- including those mapped to child devices + for idx, ep in ipairs(device.endpoints) do + if ep.endpoint_id ~= default_endpoint_id then + local id = 0 + for _, dt in ipairs(ep.device_types) do + id = math.max(id, dt.device_type_id) + end + for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do + if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and + attr ~= clusters.PowerSource.attributes.BatPercentRemaining and + attr ~= clusters.PowerSource.attributes.BatChargeLevel then + device:add_subscribed_event(attr) + else + device:add_subscribed_attribute(attr) + end + end + end + end + device:subscribe() +end + return utils 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 6ed557ed82..6371c6be44 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 @@ -14,6 +14,7 @@ local button_attr = capabilities.button.button local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x2004, product_name = "Aqara Climate Sensor W100"}, + matter_version = {hardware = 1, software = 1}, label = "Climate Sensor W100", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { 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 45d478e43d..35f4cd9ce7 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 @@ -23,6 +23,7 @@ local aqara_child2_ep = 2 local aqara_mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("4-button.yml"), manufacturer_info = {vendor_id = 0x115F, product_id = 0x1009, product_name = "Aqara Light Switch H2"}, + matter_version = {hardware = 1, software = 1}, label = "Aqara Light Switch", device_id = "00000000-1111-2222-3333-000000000001", endpoints = { 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 980bb34aba..58ba831074 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -15,6 +15,7 @@ local uint32 = require "st.matter.data_types.Uint32" local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, 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 97ffa5901e..459a69057d 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -13,6 +13,7 @@ local CAMERA_EP, FLOODLIGHT_EP, CHIME_EP, DOORBELL_EP = 1, 2, 3, 4 local mock_device = 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, 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 9f79c3c85f..301fb36cf0 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 @@ -15,6 +15,7 @@ local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("5-button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, sofrware = 1}, endpoints = { { endpoint_id = 0, @@ -92,8 +93,7 @@ local mock_device = test.mock_device.build_test_matter_device( } }, }, -} -) +}) -- add device for each mock device local CLUSTER_SUBSCRIBE_LIST ={ 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 afa2b5cb6f..dca5f20645 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 @@ -14,7 +14,8 @@ local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("6-button-motion.yml"), -- on a real device we would switch to this, rather than fingerprint to it manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - endpoints = { + matter_version = {hardware = 1, software = 1}, + endpoints = { { endpoint_id = 0, clusters = {}, 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 a036a79f18..8fec31153a 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 @@ -4,8 +4,6 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" -local utils = require "st.utils" -local dkjson = require "dkjson" local clusters = require "st.matter.generated.zap_clusters" @@ -28,6 +26,7 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -108,6 +107,7 @@ local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_te vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -195,15 +195,11 @@ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end --- All messages queued and expectations set are done before the driver is actually run local function test_init() - -- we dont want the integration test framework to generate init/doConfigure, we are doing that here - -- so we can set the proper expectations on those events. test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated test.mock_device.add_test_device(mock_child) - -- added sets a bunch of fields on the device, and calls init 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 @@ -211,11 +207,12 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - -- init results in subscription interaction test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - --doConfigure sets the provisioning state to provisioned + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) + test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) mock_device:expect_device_create({ @@ -226,19 +223,8 @@ local function test_init() parent_assigned_child_key = string.format("%d", mock_device_ep5) }) expect_configure_buttons() - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep5, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - expect_configure_buttons() - test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) @@ -462,18 +448,44 @@ test.register_coroutine_test( test.register_coroutine_test( "Test driver switched event", function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + 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.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) expect_configure_buttons() - mock_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Matter Switch 2", - profile = "light-color-level", - parent_device_id = mock_device.id, - parent_assigned_child_key = string.format("%d", mock_device_ep5) - }) end ) +test.register_coroutine_test( + "Test info changed event with parent device profile update", + function() + 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 + local updated_device_profile = t_utils.get_profile_definition("light-level-3-button.yml") + updated_device_profile.id = "updated device profile id" + 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}) + expect_configure_buttons() + end +) + +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump sw to 2 + mock_child:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-level-3-button" }) + expect_configure_buttons() + end +) + + -- run the tests test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 9cee75d69d..20606d1d41 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -13,6 +13,10 @@ local mock_device_onoff = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 4a5787346f..60891cd8bd 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -23,6 +23,10 @@ local mock_device = test.mock_device.build_test_matter_device({ vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + hardware = 1, + software = 1, + }, endpoints = { { endpoint_id = 0, @@ -687,4 +691,14 @@ test.register_coroutine_test( { test_init = test_init_parent_child_endpoints_non_sequential } ) +test.register_coroutine_test( + "Test info changed event with matter_version update", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump to 2 + mock_children[child1_ep]:expect_metadata_update({ profile = "light-level" }) + mock_children[child2_ep]:expect_metadata_update({ profile = "light-color-level" }) + mock_device:expect_metadata_update({ profile = "light-binary" }) + end +) + test.run_registered_tests() \ No newline at end of file From fb951eee3808f42866cc7f8af6e43efe97d094a6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:56:24 -0600 Subject: [PATCH 365/449] Matter Switch: Support fan/light devices as parent/child (#2653) --- .../matter-switch/fingerprints.yml | 50 ++++++++++++- .../matter-switch/profiles/fan-modular.yml | 17 +++++ .../SmartThings/matter-switch/src/init.lua | 3 +- .../src/switch_utils/device_configuration.lua | 71 +++++++++++++++++-- .../matter-switch/src/switch_utils/fields.lua | 9 ++- .../matter-switch/src/switch_utils/utils.lua | 32 +++++---- .../src/test/test_matter_camera.lua | 1 + .../src/test/test_matter_light_fan.lua | 48 +++++++++---- .../test_matter_multi_button_switch_mcd.lua | 1 + .../test/test_matter_switch_device_types.lua | 3 +- .../test_multi_switch_parent_child_lights.lua | 6 +- .../test_multi_switch_parent_child_plugs.lua | 6 +- 12 files changed, 208 insertions(+), 39 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/fan-modular.yml diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 5e65aa24fe..9a2cbbaa89 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -3587,7 +3587,7 @@ matterGeneric: deviceTypes: - id: 0x010D # Extended Color Light - id: 0x002B # Fan - deviceProfileName: light-color-level-fan + deviceProfileName: fan-modular - id: "matter/dimmable/light/motion" deviceLabel: Matter Dimmable Light Occupancy Sensor deviceTypes: @@ -3599,6 +3599,54 @@ matterGeneric: deviceTypes: - id: 0x0142 # Camera deviceProfileName: camera + - id: "matter/on-off/fan/light" + deviceLabel: Matter OnOff Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x0100 # OnOff Light + deviceProfileName: fan-modular + - id: "matter/dimmable/fan/light" + deviceLabel: Matter Dimmable Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x0101 # Dimmable Light + deviceProfileName: fan-modular + - id: "matter/colorTemperature/fan/light" + deviceLabel: Matter Color Temperature Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x010C # Color Temperature Light + deviceProfileName: fan-modular + - id: "matter/color/fan/light" + deviceLabel: Matter Color Fan Light + deviceTypes: + - id: 0x002B # Fan + - id: 0x010D # Extended Color Light + deviceProfileName: fan-modular + - id: "matter/on-off/fan/plug" + deviceLabel: Matter OnOff Fan Plug + deviceTypes: + - id: 0x002B # Fan + - id: 0x010A # On Off Plug-in Unit + deviceProfileName: fan-modular + - id: "matter/dimmable/fan/plug" + deviceLabel: Matter Dimmable Fan Plug + deviceTypes: + - id: 0x002B # Fan + - id: 0x010B # Dimmable Plug-in Unit + deviceProfileName: fan-modular + - id: "matter/mounted/on-off/control/fan" + deviceLabel: Matter Mounted OnOff Control Fan + deviceTypes: + - id: 0x002B # Fan + - id: 0x010F # Mounted On/Off Control + deviceProfileName: fan-modular + - id: "matter/mounted/dim/load/control/fan" + deviceLabel: Matter Mounted Dimmable Load Control Fan + deviceTypes: + - id: 0x002B # Fan + - id: 0x0110 # Mounted Dimmable Load Control + deviceProfileName: fan-modular matterThing: - id: SmartThings/MatterThing diff --git a/drivers/SmartThings/matter-switch/profiles/fan-modular.yml b/drivers/SmartThings/matter-switch/profiles/fan-modular.yml new file mode 100644 index 0000000000..878c9ed615 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/fan-modular.yml @@ -0,0 +1,17 @@ +name: fan-modular +components: +- id: main + capabilities: + - id: fanMode + version: 1 + optional: true + - id: fanSpeedPercent + version: 1 + optional: true + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan + diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index c2052dfe50..2fb09ca8ab 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -59,7 +59,8 @@ function SwitchLifecycleHandlers.driver_switched(driver, device) end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then + if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then + device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) if device.network_type == device_lib.NETWORK_TYPE_MATTER then device:subscribe() button_cfg.configure_buttons(device, 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 9f702ffd75..132cce2ae7 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -16,8 +16,64 @@ if version.api < 11 then end local DeviceConfiguration = {} +local ChildConfiguration = {} local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} +local FanDeviceConfiguration = {} + +function ChildConfiguration.create_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) + if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created + return + end + + table.sort(server_cluster_ep_ids) + for device_num, ep_id in ipairs(server_cluster_ep_ids) do + if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint + local label_and_name = string.format("%s %d", device.label, device_num) + local child_profile, _ = 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 + driver:try_create_device({ + type = "EDGE_CHILD", + label = label_and_name, + profile = child_profile, + parent_device_id = device.id, + parent_assigned_child_key = string.format("%d", ep_id), + vendor_provided_label = label_and_name + }) + else + existing_child_device:try_update_metadata({ + profile = child_profile + }) + end + end + end + + -- Persist so that the find_child function is always set on each driver init. + device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) + device:set_find_child(switch_utils.find_child) +end + +function FanDeviceConfiguration.assign_profile_for_fan_ep(device, server_fan_ep_id) + local ep_info = switch_utils.get_endpoint_info(device, server_fan_ep_id) + local fan_cluster_info = switch_utils.find_cluster_on_ep(ep_info, clusters.FanControl.ID) + local optional_supported_component_capabilities = {} + local main_component_capabilities = {} + + if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.MULTI_SPEED, fan_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.fanSpeedPercent.ID) + -- only fanMode can trigger AUTO, so a multi-speed fan still requires this capability if it supports AUTO + if clusters.FanControl.are_features_supported(clusters.FanControl.types.Feature.AUTO, fan_cluster_info.feature_map) then + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + else -- MULTI_SPEED is not supported + table.insert(main_component_capabilities, capabilities.fanMode.ID) + end + + table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) + return "fan-modular", optional_supported_component_capabilities +end + function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_onoff_ep_id, is_child_device) local ep_info = switch_utils.get_endpoint_info(device, server_onoff_ep_id) @@ -176,6 +232,7 @@ function DeviceConfiguration.match_profile(driver, device) if profiling_data_still_required(device) then return end local default_endpoint_id = switch_utils.find_default_endpoint(device) + local optional_component_capabilities local updated_profile if #embedded_cluster_utils.get_endpoints(device, clusters.ValveConfigurationAndControl.ID) > 0 then @@ -188,15 +245,13 @@ function DeviceConfiguration.match_profile(driver, device) local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then - SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) + ChildConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) end if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then updated_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, default_endpoint_id) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end - if generic_profile("light-color-level") and #device:get_endpoints(clusters.FanControl.ID) > 0 then - updated_profile = "light-color-level-fan" - elseif generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then + if generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then updated_profile = "light-level-motion" elseif generic_profile("plug-binary") or generic_profile("plug-level") then if switch_utils.check_switch_category_vendor_overrides(device) then @@ -209,6 +264,12 @@ function DeviceConfiguration.match_profile(driver, device) end end + local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) + if #fan_device_type_ep_ids > 0 then + updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) + device:set_field(fields.MODULAR_PROFILE_UPDATED, true) + end + -- initialize the main device card with buttons if applicable local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then @@ -219,7 +280,7 @@ function DeviceConfiguration.match_profile(driver, device) return end - device:try_update_metadata({ profile = updated_profile }) + device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities }) end return { diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f0fd0166b4..f5b85e1009 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -42,6 +42,7 @@ SwitchFields.DEVICE_TYPE_ID = { DIMMABLE_PLUG_IN_UNIT = 0x010B, DOORBELL = 0x0143, ELECTRICAL_SENSOR = 0x0510, + FAN = 0x002B, GENERIC_SWITCH = 0x000F, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, @@ -151,6 +152,8 @@ SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" --- for an Electrical Sensor EP with a "primary" endpoint, used during device profling. SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" +SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" + SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", } @@ -255,7 +258,8 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode }, [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = { clusters.OnOff.attributes.OnOff @@ -286,7 +290,8 @@ SwitchFields.device_type_attribute_map = { clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode }, [SwitchFields.DEVICE_TYPE_ID.GENERIC_SWITCH] = { clusters.PowerSource.attributes.BatPercentRemaining, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 7bbb5c55e8..88c846501a 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -151,8 +151,9 @@ function utils.find_default_endpoint(device) return device.MATTER_DEFAULT_ENDPOINT end - local switch_eps = device:get_endpoints(clusters.OnOff.ID) - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + local onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + local fan_endpoint_ids = utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) local get_first_non_zero_endpoint = function(endpoints) table.sort(endpoints) @@ -164,26 +165,31 @@ function utils.find_default_endpoint(device) return nil end - -- Return the first switch endpoint as the default endpoint if no button endpoints are present - if #button_eps == 0 and #switch_eps > 0 then - return get_first_non_zero_endpoint(switch_eps) + -- Return the first fan endpoint as the default endpoint if any is found + if #fan_endpoint_ids > 0 then + return get_first_non_zero_endpoint(fan_endpoint_ids) end - -- Return the first button endpoint as the default endpoint if no switch endpoints are present - if #switch_eps == 0 and #button_eps > 0 then - return get_first_non_zero_endpoint(button_eps) + -- Return the first onoff endpoint as the default endpoint if no momentary switch endpoints are present + if #momentary_switch_ep_ids == 0 and #onoff_ep_ids > 0 then + return get_first_non_zero_endpoint(onoff_ep_ids) end - -- If both switch and button endpoints are present, check the device type on the main switch - -- endpoint. If it is not a supported device type, return the first button endpoint as the + -- Return the first momentary switch endpoint as the default endpoint if no onoff endpoints are present + if #onoff_ep_ids == 0 and #momentary_switch_ep_ids > 0 then + return get_first_non_zero_endpoint(momentary_switch_ep_ids) + end + + -- If both onoff and momentary switch endpoints are present, check the device type on the first onoff + -- endpoint. If it is not a supported device type, return the first momentary switch endpoint as the -- default endpoint. - if #switch_eps > 0 and #button_eps > 0 then - local default_endpoint_id = get_first_non_zero_endpoint(switch_eps) + if #onoff_ep_ids > 0 and #momentary_switch_ep_ids > 0 then + local default_endpoint_id = get_first_non_zero_endpoint(onoff_ep_ids) if utils.device_type_supports_button_switch_combination(device, default_endpoint_id) then return default_endpoint_id else device.log.warn("The main switch endpoint does not contain a supported device type for a component configuration with buttons") - return get_first_non_zero_endpoint(button_eps) + return get_first_non_zero_endpoint(momentary_switch_ep_ids) end 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 459a69057d..95bef23644 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -316,6 +316,7 @@ local function update_device_profile() clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, clusters.OccupancySensing.attributes.Occupancy, clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, 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 a35f5bda50..676a376726 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 @@ -15,12 +15,16 @@ local mock_device_ep1 = 1 local mock_device_ep2 = 2 local mock_device = test.mock_device.build_test_matter_device({ - label = "Matter Switch", - profile = t_utils.get_profile_definition("light-color-level-fan.yml"), + label = "Matter Fan Light", + profile = t_utils.get_profile_definition("fan-modular.yml", {}), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, }, + matter_version = { + software = 1, + hardware = 1, + }, endpoints = { { endpoint_id = 0, @@ -34,7 +38,7 @@ local mock_device = test.mock_device.build_test_matter_device({ { endpoint_id = mock_device_ep1, clusters = { - {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", feature_map = 0}, {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER", feature_map = 2}, {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, }, @@ -72,16 +76,24 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.FanControl.attributes.PercentCurrent, } +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-color-level.yml"), + device_network_id = string.format("%s:%d", mock_device.id, 4), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep1) +}) + local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) 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.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -- since all fan capabilities are optional, nothing is initially subscribed to test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -89,8 +101,21 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.matter:__expect_send({mock_device.id, clusters.LevelControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, mock_device_ep1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device:expect_metadata_update({ profile = "light-color-level-fan" }) + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Matter Fan Light 1", + profile = "light-color-level", + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%d", mock_device_ep1) + }) + mock_device:expect_metadata_update({ profile = "fan-modular", optional_component_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}} }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + local updated_device_profile = t_utils.get_profile_definition("fan-modular.yml", + {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}} + ) + 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 test.set_test_init_function(test_init) @@ -99,7 +124,7 @@ test.register_coroutine_test( "Switch capability should send the appropriate commands", function() test.socket.capability:__queue_receive( { - mock_device.id, + mock_child.id, { capability = "switch", component = "main", command = "on", args = { } } } ) @@ -107,14 +132,14 @@ test.register_coroutine_test( test.socket.devices:__expect_send( { "register_native_capability_cmd_handler", - { device_uuid = mock_device.id, capability_id = "switch", capability_cmd_id = "on" } + { device_uuid = mock_child.id, capability_id = "switch", capability_cmd_id = "on" } } ) end test.socket.matter:__expect_send( { mock_device.id, - clusters.OnOff.server.commands.On(mock_device, 1) + clusters.OnOff.server.commands.On(mock_device, mock_device_ep1) } ) test.socket.matter:__queue_receive( @@ -129,9 +154,8 @@ test.register_coroutine_test( { device_uuid = mock_device.id, capability_id = "switch", capability_attr_id = "switch" } } ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( + mock_child:generate_test_message( "main", capabilities.switch.switch.on() ) ) @@ -145,7 +169,7 @@ test.register_message_test( channel = "capability", direction = "receive", message = { - mock_device.id, + mock_child.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } } }, @@ -176,7 +200,7 @@ test.register_message_test( { channel = "capability", direction = "send", - message = mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) + message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) }, } ) 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 8fec31153a..49fd69d0ec 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 @@ -178,6 +178,7 @@ local CLUSTER_SUBSCRIBE_LIST ={ clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.ShortRelease, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index 20606d1d41..f90855a308 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -607,7 +607,8 @@ local function test_init_parent_child_different_types() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) for i, cluster in ipairs(cluster_subscribe_list) do diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 60891cd8bd..e218371df4 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -166,7 +166,8 @@ local function test_init() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do @@ -243,7 +244,8 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode } local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) for i, cluster in ipairs(cluster_subscribe_list) do diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 215c8f34ed..9ac3642a63 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -162,7 +162,8 @@ local function test_init() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) for i, cluster in ipairs(cluster_subscribe_list) do @@ -237,7 +238,8 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentHue, clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) for i, cluster in ipairs(cluster_subscribe_list) do From 05aa526eedbf28c42d25e146a986850ca489b571 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 9 Jan 2026 11:26:18 -0600 Subject: [PATCH 366/449] add electrical handling to driverSwitched --- drivers/SmartThings/matter-switch/src/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 2fb09ca8ab..5a834c0385 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -54,6 +54,7 @@ end function SwitchLifecycleHandlers.driver_switched(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then + switch_utils.handle_electrical_sensor_info(device) -- field settings required for proper setup when switching drivers device_cfg.match_profile(driver, device) end end From 3de35fd2f8cddbfade10a58d61fe38e41c9d2659 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Sat, 13 Dec 2025 22:00:05 -0600 Subject: [PATCH 367/449] update subscribe for main driver and camera subdriver --- .../SmartThings/matter-switch/src/init.lua | 44 +++- .../sub_drivers/camera/camera_utils/utils.lua | 79 +++--- .../src/sub_drivers/camera/init.lua | 2 +- .../switch_handlers/attribute_handlers.lua | 14 +- .../matter-switch/src/switch_utils/fields.lua | 131 +--------- .../matter-switch/src/switch_utils/utils.lua | 70 +++-- .../src/test/test_eve_energy.lua | 2 +- .../src/test/test_matter_bridge.lua | 19 -- .../src/test/test_matter_camera.lua | 242 +++++++++--------- .../test_matter_multi_button_switch_mcd.lua | 139 +++++----- .../src/test/test_matter_switch.lua | 1 + .../test/test_matter_switch_device_types.lua | 27 +- .../test_multi_switch_parent_child_lights.lua | 2 +- .../test_multi_switch_parent_child_plugs.lua | 2 +- 14 files changed, 348 insertions(+), 426 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 5a834c0385..6655f81e56 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -49,6 +49,10 @@ function SwitchLifecycleHandlers.do_configure(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then switch_cfg.set_device_control_options(device) device_cfg.match_profile(driver, device) + elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then + -- because get_parent_device() may cause race conditions if used in init, an initial child subscribe is handled in doConfigure. + -- all future calls to subscribe will be handled by the parent device in init + device:subscribe() end end @@ -68,7 +72,7 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) ) elseif device.network_type == device_lib.NETWORK_TYPE_CHILD then - switch_utils.update_subscriptions(device:get_parent_device()) -- parent device required to scan through EPs and update subscriptions + device:get_parent_device():subscribe() -- parent device required to send subscription requests end end @@ -87,7 +91,8 @@ function SwitchLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end - switch_utils.update_subscriptions(device) + device:extend_device("subscribe", switch_utils.subscribe) + device:subscribe() -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. @@ -290,7 +295,40 @@ local matter_driver_template = { [capabilities.valve.commands.open.NAME] = capability_handlers.handle_valve_open, }, }, - supported_capabilities = fields.supported_capabilities, + supported_capabilities = { + capabilities.audioMute, + capabilities.audioRecording, + capabilities.audioVolume, + capabilities.battery, + capabilities.batteryLevel, + capabilities.button, + capabilities.cameraPrivacyMode, + capabilities.cameraViewportSettings, + capabilities.colorControl, + capabilities.colorTemperature, + capabilities.energyMeter, + capabilities.fanMode, + capabilities.fanSpeedPercent, + capabilities.hdr, + capabilities.illuminanceMeasurement, + capabilities.imageControl, + capabilities.level, + capabilities.localMediaStorage, + capabilities.mechanicalPanTiltZoom, + capabilities.motionSensor, + capabilities.nightVision, + capabilities.powerMeter, + capabilities.powerConsumptionReport, + capabilities.relativeHumidityMeasurement, + capabilities.sounds, + capabilities.switch, + capabilities.switchLevel, + capabilities.temperatureMeasurement, + capabilities.valve, + capabilities.videoStreamSettings, + capabilities.webrtc, + capabilities.zoneManagement + }, sub_drivers = { switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index dcdd936950..f08e757553 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -193,7 +193,8 @@ function CameraUtils.subscribe(device) clusters.CameraAvStreamManagement.attributes.StatusLightBrightness }, [capabilities.switch.ID] = { - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.OnOff.attributes.OnOff }, [capabilities.videoStreamSettings.ID] = { clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, @@ -223,8 +224,26 @@ function CameraUtils.subscribe(device) }, [capabilities.motionSensor.ID] = { clusters.OccupancySensing.attributes.Occupancy - } + }, + [capabilities.switchLevel.ID] = { + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + }, + [capabilities.colorControl.ID] = { + clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + }, + [capabilities.colorTemperature.ID] = { + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + }, } + local camera_subscribed_events = { [capabilities.zoneManagement.ID] = { clusters.ZoneManagement.events.ZoneTriggered, @@ -238,56 +257,26 @@ function CameraUtils.subscribe(device) } } - for capability, attr_list in pairs(camera_subscribed_attributes) do - if device:supports_capability_by_id(capability) then - for _, attr in pairs(attr_list) do - device:add_subscribed_attribute(attr) - end - end - end - for capability, event_list in pairs(camera_subscribed_events) do - if device:supports_capability_by_id(capability) then - for _, event in pairs(event_list) do - device:add_subscribed_event(event) - end - end - end + local im = require "st.matter.interaction_model" + + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} - -- match_profile is called from the CameraAvStreamManagement AttributeList handler, - -- so the subscription needs to be added here first if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) > 0 then - device:add_subscribed_attribute(clusters.CameraAvStreamManagement.attributes.AttributeList) + local ib = im.InteractionInfoBlock(nil, clusters.CameraAvStreamManagement.ID, clusters.CameraAvStreamManagement.attributes.AttributeList.ID) + subscribe_request:with_info_block(ib) end - -- Add subscription for attributes specific to child devices - if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then - for _, ep in ipairs(device.endpoints or {}) do - local id = 0 - for _, dt in ipairs(ep.device_types or {}) do - if dt.device_type_id ~= fields.DEVICE_TYPE_ID.GENERIC_SWITCH then - id = math.max(id, dt.device_type_id) - end - end - for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do - device:add_subscribed_attribute(attr) - end + for _, endpoint_info in ipairs(device.endpoints) do + local checked_device = switch_utils.find_child(device, endpoint_info.endpoint_id) or device + if not devices_seen[checked_device.id] then + switch_utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + camera_subscribed_attributes, camera_subscribed_events + ) + devices_seen[checked_device.id] = true -- only loop through any device once end end - local im = require "st.matter.interaction_model" - local subscribed_attributes = device:get_field("__subscribed_attributes") or {} - local subscribed_events = device:get_field("__subscribed_events") or {} - local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) - for _, attributes in pairs(subscribed_attributes) do - for _, ib in pairs(attributes) do - subscribe_request:with_info_block(ib) - end - end - for _, events in pairs(subscribed_events) do - for _, ib in pairs(events) do - subscribe_request:with_info_block(ib) - end - end if #subscribe_request.info_blocks > 0 then device:send(subscribe_request) end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index e8d5aad8fa..f13589ff41 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -49,10 +49,10 @@ end function CameraLifecycleHandlers.info_changed(driver, device, event, args) if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then camera_cfg.initialize_camera_capabilities(device) + device:subscribe() if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) end - device:subscribe() end end 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 1a36001d6c..e8dfe3f120 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -139,7 +139,7 @@ function AttributeHandlers.color_temperature_mireds_handler(driver, device, ib, end function AttributeHandlers.current_x_handler(driver, device, ib, response) - if device:get_field(fields.COLOR_MODE) == fields.HUE_SAT_COLOR_MODE then + if device:get_field(fields.COLOR_MODE) == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then return end local y = device:get_field(fields.RECEIVED_Y) @@ -157,7 +157,7 @@ function AttributeHandlers.current_x_handler(driver, device, ib, response) end function AttributeHandlers.current_y_handler(driver, device, ib, response) - if device:get_field(fields.COLOR_MODE) == fields.HUE_SAT_COLOR_MODE then + if device:get_field(fields.COLOR_MODE) == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then return end local x = device:get_field(fields.RECEIVED_X) @@ -173,15 +173,17 @@ function AttributeHandlers.current_y_handler(driver, device, ib, response) end function AttributeHandlers.color_mode_handler(driver, device, ib, response) - if ib.data.value == device:get_field(fields.COLOR_MODE) or (ib.data.value ~= fields.HUE_SAT_COLOR_MODE and ib.data.value ~= fields.X_Y_COLOR_MODE) then - return + if ib.data.value == device:get_field(fields.COLOR_MODE) + or (ib.data.value ~= clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION + and ib.data.value ~= clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) then + return end device:set_field(fields.COLOR_MODE, ib.data.value) local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - if ib.data.value == fields.HUE_SAT_COLOR_MODE then + if ib.data.value == clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION then req:merge(clusters.ColorControl.attributes.CurrentHue:read()) req:merge(clusters.ColorControl.attributes.CurrentSaturation:read()) - elseif ib.data.value == fields.X_Y_COLOR_MODE then + elseif ib.data.value == clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY then req:merge(clusters.ColorControl.attributes.CurrentX:read()) req:merge(clusters.ColorControl.attributes.CurrentY:read()) end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f5b85e1009..f670b154c8 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,27 +1,13 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local clusters = require "st.matter.clusters" -local capabilities = require "st.capabilities" -local version = require "version" - --- Include driver-side definitions when lua libs api version is < 11 -if version.api < 11 then - clusters.ElectricalEnergyMeasurement = require "embedded_clusters.ElectricalEnergyMeasurement" - clusters.ElectricalPowerMeasurement = require "embedded_clusters.ElectricalPowerMeasurement" -end - local SwitchFields = {} -SwitchFields.HUE_SAT_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION -SwitchFields.X_Y_COLOR_MODE = clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY - SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" SwitchFields.RECEIVED_X = "receivedX" SwitchFields.RECEIVED_Y = "receivedY" SwitchFields.HUESAT_SUPPORT = "huesatSupport" - SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 -- These values are a "sanity check" to check that values we are getting are reasonable @@ -192,119 +178,4 @@ SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second SwitchFields.OPTIONS_MASK = 0x01 SwitchFields.OPTIONS_OVERRIDE = 0x01 - -SwitchFields.supported_capabilities = { - capabilities.audioMute, - capabilities.audioRecording, - capabilities.audioVolume, - capabilities.battery, - capabilities.batteryLevel, - capabilities.button, - capabilities.cameraPrivacyMode, - capabilities.cameraViewportSettings, - capabilities.colorControl, - capabilities.colorTemperature, - capabilities.energyMeter, - capabilities.fanMode, - capabilities.fanSpeedPercent, - capabilities.hdr, - capabilities.illuminanceMeasurement, - capabilities.imageControl, - capabilities.level, - capabilities.localMediaStorage, - capabilities.mechanicalPanTiltZoom, - capabilities.motionSensor, - capabilities.nightVision, - capabilities.powerMeter, - capabilities.powerConsumptionReport, - capabilities.relativeHumidityMeasurement, - capabilities.sounds, - capabilities.switch, - capabilities.switchLevel, - capabilities.temperatureMeasurement, - capabilities.valve, - capabilities.videoStreamSettings, - capabilities.webrtc, - capabilities.zoneManagement -} - -SwitchFields.device_type_attribute_map = { - [SwitchFields.DEVICE_TYPE_ID.LIGHT.ON_OFF] = { - clusters.OnOff.attributes.OnOff - }, - [SwitchFields.DEVICE_TYPE_ID.LIGHT.DIMMABLE] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [SwitchFields.DEVICE_TYPE_ID.LIGHT.COLOR_TEMPERATURE] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds - }, - [SwitchFields.DEVICE_TYPE_ID.LIGHT.EXTENDED_COLOR] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode - }, - [SwitchFields.DEVICE_TYPE_ID.ON_OFF_PLUG_IN_UNIT] = { - clusters.OnOff.attributes.OnOff - }, - [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [SwitchFields.DEVICE_TYPE_ID.SWITCH.ON_OFF_LIGHT] = { - clusters.OnOff.attributes.OnOff - }, - [SwitchFields.DEVICE_TYPE_ID.SWITCH.DIMMER] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel - }, - [SwitchFields.DEVICE_TYPE_ID.SWITCH.COLOR_DIMMER] = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode - }, - [SwitchFields.DEVICE_TYPE_ID.GENERIC_SWITCH] = { - clusters.PowerSource.attributes.BatPercentRemaining, - clusters.Switch.events.InitialPress, - clusters.Switch.events.LongPress, - clusters.Switch.events.ShortRelease, - clusters.Switch.events.MultiPressComplete - }, - [SwitchFields.DEVICE_TYPE_ID.ELECTRICAL_SENSOR] = { - clusters.ElectricalPowerMeasurement.attributes.ActivePower, - clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported, - clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported - } -} - -return SwitchFields \ No newline at end of file +return SwitchFields diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 88c846501a..c6299fba95 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -433,27 +433,63 @@ function utils.lazy_load_if_possible(sub_driver_name) end end -function utils.update_subscriptions(device) - local default_endpoint_id = utils.find_default_endpoint(device) - -- ensure subscription to all endpoint attributes- including those mapped to child devices - for idx, ep in ipairs(device.endpoints) do - if ep.endpoint_id ~= default_endpoint_id then - local id = 0 - for _, dt in ipairs(ep.device_types) do - id = math.max(id, dt.device_type_id) - end - for _, attr in pairs(fields.device_type_attribute_map[id] or {}) do - if id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH and - attr ~= clusters.PowerSource.attributes.BatPercentRemaining and - attr ~= clusters.PowerSource.attributes.BatChargeLevel then - device:add_subscribed_event(attr) - else - device:add_subscribed_attribute(attr) +--- helper for the switch subscribe override, which adds to a subscribed request for a checked device +--- +--- @param checked_device any a Matter device object, either a parent or child device, so not necessarily the same as device +--- @param subscribe_request table a subscribe request that will be appended to as needed for the device +--- @param capabilities_seen table a list of capabilities that have already been checked by previously handled devices +--- @param attributes_seen table a list of attributes that have already been checked +--- @param events_seen table a list of events that have already been checked +--- @param subscribed_attributes table key-value pairs mapping capability ids to subscribed attributes +--- @param subscribed_events table key-value pairs mapping capability ids to subscribed events +function utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, subscribed_attributes, subscribed_events) + for _, component in pairs(checked_device.st_store.profile.components) do + for _, capability in pairs(component.capabilities) do + if not capabilities_seen[capability.id] then + for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do + local cluster_id = attr.cluster or attr._cluster.ID + local attr_id = attr.ID or attr.attribute + if not attributes_seen[cluster_id..attr_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) + subscribe_request:with_info_block(ib) + attributes_seen[cluster_id..attr_id] = true + end end + for _, event in ipairs(subscribed_events[capability.id] or {}) do + local cluster_id = event.cluster or event._cluster.ID + local event_id = event.ID or event.event + if not events_seen[cluster_id..event_id] then + local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) + subscribe_request:with_info_block(ib) + events_seen[cluster_id..event_id] = true + end + end + capabilities_seen[capability.id] = true -- only loop through any capability once end end end - device:subscribe() +end + +--- create and send a subscription request by checking all devices, accounting for both parent and child devices +--- +--- @param device any a Matter device object +function utils.subscribe(device) + local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) + local devices_seen, capabilities_seen, attributes_seen, events_seen = {}, {}, {}, {} + + for _, endpoint_info in ipairs(device.endpoints) do + local checked_device = utils.find_child(device, endpoint_info.endpoint_id) or device + if not devices_seen[checked_device.id] then + utils.populate_subscribe_request_for_device(checked_device, subscribe_request, capabilities_seen, attributes_seen, events_seen, + device.driver.subscribed_attributes, device.driver.subscribed_events + ) + devices_seen[checked_device.id] = true -- only loop through any device once + end + end + + if #subscribe_request.info_blocks > 0 then + device:send(subscribe_request) + end end return utils 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 1598f953a4..a189b1e5fc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -55,7 +55,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_electrical_sensor = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("power-energy-powerConsumption.yml"), + profile = t_utils.get_profile_definition("plug-energy-powerConsumption.yml"), manufacturer_info = { vendor_id = 0x130A, product_id = 0x0050, 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 1aaef32d37..c696815a3e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -59,34 +59,15 @@ local mock_bridge = test.mock_device.build_test_matter_device({ } }) -local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff -} - local function test_init_mock_bridge() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_bridge) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_bridge)) - end - end - test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) test.mock_device.add_test_device(mock_bridge) end test.register_coroutine_test( "Profile should not change for devices with aggregator device type (bridges)", function() - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_bridge) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_bridge)) - end - end test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) - test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) - test.socket.matter:__expect_send({mock_bridge.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) 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 95bef23644..a327c39c31 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -122,6 +122,19 @@ local mock_device = test.mock_device.build_test_matter_device({ local subscribe_request local subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, } local function test_init() @@ -153,60 +166,125 @@ end test.set_test_init_function(test_init) -local function update_device_profile() - test.socket.matter:__set_channel_ordering("relaxed") - local uint32 = require "st.matter.data_types.Uint32" - local expected_metadata = { - optional_component_capabilities = { - { - "main", - { - "videoCapture2", - "cameraViewportSettings", - "localMediaStorage", - "audioRecording", - "cameraPrivacyMode", - "imageControl", - "hdr", - "nightVision", - "mechanicalPanTiltZoom", - "videoStreamSettings", - "zoneManagement", - "webrtc", - "motionSensor", - "sounds", - } - }, +local additional_subscribed_attributes = { + clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, + clusters.CameraAvStreamManagement.attributes.ImageRotation, + clusters.CameraAvStreamManagement.attributes.NightVision, + clusters.CameraAvStreamManagement.attributes.NightVisionIllum, + clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, + clusters.CameraAvStreamManagement.attributes.ImageFlipVertical, + clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, + clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn, + clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport, + clusters.CameraAvStreamManagement.attributes.SpeakerMuted, + clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, + clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, + clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, + clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.AttributeList, + 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, + clusters.Chime.attributes.InstalledChimeSounds, + clusters.Chime.attributes.SelectedChime, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.OccupancySensing.attributes.Occupancy, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete +} + +local expected_metadata = { + optional_component_capabilities = { + { + "main", { - "statusLed", - { - "switch", - "mode" - } - }, + "videoCapture2", + "cameraViewportSettings", + "localMediaStorage", + "audioRecording", + "cameraPrivacyMode", + "imageControl", + "hdr", + "nightVision", + "mechanicalPanTiltZoom", + "videoStreamSettings", + "zoneManagement", + "webrtc", + "motionSensor", + "sounds", + } + }, + { + "statusLed", { - "speaker", - { - "audioMute", - "audioVolume" - } - }, + "switch", + "mode" + } + }, + { + "speaker", { - "microphone", - { - "audioMute", - "audioVolume" - } - }, + "audioMute", + "audioVolume" + } + }, + { + "microphone", { - "doorbell", - { - "button" - } + "audioMute", + "audioVolume" } }, - profile = "camera" - } + { + "doorbell", + { + "button" + } + } + }, + profile = "camera" +} + +local function update_device_profile() + local uint32 = require "st.matter.data_types.Uint32" test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { @@ -219,6 +297,7 @@ local function update_device_profile() 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:generate_info_changed({ profile = updated_device_profile })) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.webrtc.supportedFeatures( @@ -258,77 +337,12 @@ local function update_device_profile() {"setSoftRecordingPrivacyMode", "setSoftLivestreamPrivacyMode"} )) ) - local additional_subscribed_attributes = { - clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, - clusters.CameraAvStreamManagement.attributes.ImageRotation, - clusters.CameraAvStreamManagement.attributes.NightVision, - clusters.CameraAvStreamManagement.attributes.NightVisionIllum, - clusters.CameraAvStreamManagement.attributes.ImageFlipHorizontal, - clusters.CameraAvStreamManagement.attributes.ImageFlipVertical, - clusters.CameraAvStreamManagement.attributes.SoftRecordingPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.SoftLivestreamPrivacyModeEnabled, - clusters.CameraAvStreamManagement.attributes.HardPrivacyModeOn, - clusters.CameraAvStreamManagement.attributes.TwoWayTalkSupport, - clusters.CameraAvStreamManagement.attributes.SpeakerMuted, - clusters.CameraAvStreamManagement.attributes.MicrophoneMuted, - clusters.CameraAvStreamManagement.attributes.SpeakerVolumeLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMaxLevel, - clusters.CameraAvStreamManagement.attributes.SpeakerMinLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneVolumeLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMaxLevel, - clusters.CameraAvStreamManagement.attributes.MicrophoneMinLevel, - clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, - clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, - clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, - clusters.CameraAvStreamManagement.attributes.LocalSnapshotRecordingEnabled, - clusters.CameraAvStreamManagement.attributes.LocalVideoRecordingEnabled, - clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, - clusters.CameraAvStreamManagement.attributes.VideoSensorParams, - clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, - clusters.CameraAvStreamManagement.attributes.Viewport, - clusters.CameraAvStreamManagement.attributes.MinViewportResolution, - clusters.CameraAvStreamManagement.attributes.AttributeList, - 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, - clusters.Chime.attributes.InstalledChimeSounds, - clusters.Chime.attributes.SelectedChime, - clusters.ZoneManagement.attributes.MaxZones, - clusters.ZoneManagement.attributes.Zones, - clusters.ZoneManagement.attributes.Triggers, - clusters.ZoneManagement.attributes.SensitivityMax, - clusters.ZoneManagement.attributes.Sensitivity, - clusters.ZoneManagement.events.ZoneTriggered, - clusters.ZoneManagement.events.ZoneStopped, - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.OccupancySensing.attributes.Occupancy, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete - } for _, attr in ipairs(additional_subscribed_attributes) do subscribe_request:merge(attr:subscribe(mock_device)) end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end -- Matter Handler UTs 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 49fd69d0ec..d412c2b35e 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 @@ -102,7 +102,7 @@ local mock_device = test.mock_device.build_test_matter_device({ local mock_device_mcd_unsupported_switch_device_type = test.mock_device.build_test_matter_device({ label = "Matter Switch", - profile = t_utils.get_profile_definition("matter-thing.yml"), + profile = t_utils.get_profile_definition("button.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -166,7 +166,18 @@ local child_data = { local mock_child = test.mock_device.build_test_child_device(child_data) -- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ +local CLUSTER_SUBSCRIBE_LIST_NO_CHILD ={ + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, +} + +local CLUSTER_SUBSCRIBE_LIST_WITH_CHILD ={ clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, clusters.LevelControl.attributes.MaxLevel, @@ -199,10 +210,10 @@ end local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated - test.mock_device.add_test_device(mock_child) - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + -- added sets a bunch of fields on the device, and calls init + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -225,12 +236,6 @@ local function test_init() }) expect_configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - - test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) - test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) - mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) end -- All messages queued and expectations set are done before the driver is actually run @@ -350,63 +355,45 @@ test.register_coroutine_test( end ) -test.register_message_test( +test.register_coroutine_test( "Switch child device: Set color temperature should send the appropriate commands", - { - { - channel = "capability", - direction = "receive", - message = { - mock_child.id, - { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } - } - }, - { - channel = "matter", - direction = "send", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) - } - }, - { - channel = "capability", - direction = "send", - message = mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800)) - }, - } + function() + test.mock_device.add_test_device(mock_child) + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_child.id, + { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = {1800} } + }) + test.socket.matter:__expect_send({ + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + }) + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ColorControl.server.commands.MoveToColorTemperature:build_test_command_response(mock_device, mock_device_ep5) + }) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ColorControl.attributes.ColorTemperatureMireds:build_test_report_data(mock_device, mock_device_ep5, 556) + }) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) + end ) test.register_coroutine_test( "Test MCD configuration not including switch for unsupported switch device type, create child device instead", function() + local unsup_mock_device = mock_device_mcd_unsupported_switch_device_type -- added sets a bunch of fields on the device, and calls init - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, + local CLUSTER_SUBSCRIBE_LIST = { clusters.Switch.server.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.ShortRelease, clusters.Switch.server.events.MultiPressComplete, } - local unsup_mock_device = mock_device_mcd_unsupported_switch_device_type - local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) - for _, cluster in ipairs(cluster_subscribe_list) do + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(unsup_mock_device) + for _, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end test.socket.device_lifecycle:__queue_receive({ unsup_mock_device.id, "added" }) @@ -423,14 +410,24 @@ test.register_coroutine_test( parent_device_id = unsup_mock_device.id, parent_assigned_child_key = string.format("%d", 7) }) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) unsup_mock_device:expect_metadata_update({ profile = "2-button" }) unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() local updated_device_profile = t_utils.get_profile_definition("2-button.yml") test.socket.device_lifecycle:__queue_receive(unsup_mock_device:generate_info_changed({ profile = updated_device_profile })) - local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do + + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(unsup_mock_device) + for i, cluster in ipairs(CLUSTER_SUBSCRIBE_LIST) do if i > 1 then subscribe_request:merge(cluster:subscribe(unsup_mock_device)) end @@ -449,9 +446,10 @@ test.register_coroutine_test( test.register_coroutine_test( "Test driver switched event", function() + test.mock_device.add_test_device(mock_child) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_WITH_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_WITH_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -465,8 +463,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Test info changed event with parent device profile update", function() - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device) - for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_NO_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_NO_CHILD) do if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end end local updated_device_profile = t_utils.get_profile_definition("light-level-3-button.yml") @@ -480,6 +478,7 @@ test.register_coroutine_test( test.register_coroutine_test( "Test info changed event with matter_version update", function() + test.mock_device.add_test_device(mock_child) test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = { hardware = 1, software = 2 } })) -- bump sw to 2 mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) @@ -487,6 +486,22 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Test child device initialization, and that subscriptions are initialized correctly", + function () + test.mock_device.add_test_device(mock_child) + test.socket.matter:__expect_send({mock_device.id, clusters.OnOff.attributes.OnOff:read(mock_device)}) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "init" }) + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_WITH_CHILD[1]:subscribe(mock_device) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_WITH_CHILD) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + mock_child:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) + end +) -- run the tests test.run_registered_tests() 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 25bc0fdfaa..6a5ecd2cc4 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -126,6 +126,7 @@ local cluster_subscribe_list = { clusters.ColorControl.attributes.ColorTemperatureMireds, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.ColorControl.attributes.ColorMode, } local function set_color_mode(device, endpoint, color_mode) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua index f90855a308..5513c915b7 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch_device_types.lua @@ -439,14 +439,8 @@ local mock_device_light_level_motion = test.mock_device.build_test_matter_device local function test_init_parent_child_switch_types() test.mock_device.add_test_device(mock_device_parent_child_switch_types) - local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_child_switch_types) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "init" }) - test.socket.matter:__expect_send({mock_device_parent_child_switch_types.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_switch_types.id, "doConfigure" }) test.socket.matter:__expect_send({ mock_device_parent_child_switch_types.id, @@ -483,13 +477,10 @@ end local function test_init_parent_client_child_server() test.mock_device.add_test_device(mock_device_parent_client_child_server) - local subscribe_request = clusters.OnOff.attributes.OnOff:subscribe(mock_device_parent_client_child_server) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "added" }) - test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "init" }) - test.socket.matter:__expect_send({mock_device_parent_client_child_server.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_parent_client_child_server.id, "doConfigure" }) mock_device_parent_client_child_server:expect_metadata_update({ profile = "switch-binary" }) @@ -597,25 +588,9 @@ end local function test_init_parent_child_different_types() test.mock_device.add_test_device(mock_device_parent_child_different_types) local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, + clusters.OnOff.attributes.OnOff } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_different_types) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device_parent_child_different_types)) - end - end test.socket.device_lifecycle:__queue_receive({ mock_device_parent_child_different_types.id, "added" }) test.socket.matter:__expect_send({mock_device_parent_child_different_types.id, subscribe_request}) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index e218371df4..c0ed244d51 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -245,7 +245,7 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(unsup_mock_device) for i, cluster in ipairs(cluster_subscribe_list) do diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 9ac3642a63..09fa39c4f6 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -239,7 +239,7 @@ local function test_init_parent_child_endpoints_non_sequential() clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.ColorMode, } local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_parent_child_endpoints_non_sequential) for i, cluster in ipairs(cluster_subscribe_list) do From 8422dd5f5fd2fef9f854d4a9b1c1fcf93f8300fa Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 12 Jan 2026 10:51:01 -0600 Subject: [PATCH 368/449] remove unused helper function, logic moved to generic child handler --- .../src/switch_utils/device_configuration.lua | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) 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 132cce2ae7..7ee4b6bf64 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -21,7 +21,7 @@ local SwitchDeviceConfiguration = {} local ButtonDeviceConfiguration = {} local FanDeviceConfiguration = {} -function ChildConfiguration.create_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) +function ChildConfiguration.create_or_update_child_devices(driver, device, server_cluster_ep_ids, default_endpoint_id, assign_profile_fn) if #server_cluster_ep_ids == 1 and server_cluster_ep_ids[1] == default_endpoint_id then -- no children will be created return end @@ -99,39 +99,6 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on return generic_profile or "switch-binary" end -function SwitchDeviceConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id) - if #server_onoff_ep_ids == 1 and server_onoff_ep_ids[1] == default_endpoint_id then -- no children will be created - return - end - - table.sort(server_onoff_ep_ids) - for device_num, ep_id in ipairs(server_onoff_ep_ids) do - if ep_id ~= default_endpoint_id then -- don't create a child device that maps to the main endpoint - local label_and_name = string.format("%s %d", device.label, device_num) - local child_profile = SwitchDeviceConfiguration.assign_profile_for_onoff_ep(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 - driver:try_create_device({ - type = "EDGE_CHILD", - label = label_and_name, - profile = child_profile, - parent_device_id = device.id, - parent_assigned_child_key = string.format("%d", ep_id), - vendor_provided_label = label_and_name - }) - else - existing_child_device:try_update_metadata({ - profile = child_profile - }) - end - end - end - - -- Persist so that the find_child function is always set on each driver init. - device:set_field(fields.IS_PARENT_CHILD_DEVICE, true, {persist = true}) - device:set_find_child(switch_utils.find_child) -end - -- Per the spec, these attributes are "meant to be changed only during commissioning." function SwitchDeviceConfiguration.set_device_control_options(device) for _, ep in ipairs(device.endpoints) do @@ -245,7 +212,7 @@ function DeviceConfiguration.match_profile(driver, device) local server_onoff_ep_ids = device:get_endpoints(clusters.OnOff.ID) -- get_endpoints defaults to return EPs supporting SERVER or BOTH if #server_onoff_ep_ids > 0 then - ChildConfiguration.create_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) + ChildConfiguration.create_or_update_child_devices(driver, device, server_onoff_ep_ids, default_endpoint_id, SwitchDeviceConfiguration.assign_profile_for_onoff_ep) end if switch_utils.tbl_contains(server_onoff_ep_ids, default_endpoint_id) then From 1d911f954e93765a76ad1cdff00f8146539c5891 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:34:50 -0600 Subject: [PATCH 369/449] Matter Camera: Only update profile if capabilities have changed (#2686) The `optional_capabilities_list_changed` function was not properly comparing the new set of supported capabilities with the current set, meaning that a profile update was occurring each time `camera_av_stream_management_attribute_list_handler` runs. This commit fixes `optional_capabilities_list_changed` and provides new test cases to ensure that the extraneous profile updates no longer occur. --- .../camera_utils/device_configuration.lua | 6 +- .../sub_drivers/camera/camera_utils/utils.lua | 51 +++++++++++----- .../src/test/test_matter_camera.lua | 59 ++++++++++++++++++- 3 files changed, 97 insertions(+), 19 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 6626729c78..c84337736d 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 @@ -121,8 +121,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr local doorbell_endpoints = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) if #doorbell_endpoints > 0 then table.insert(doorbell_component_capabilities, capabilities.button.ID) - CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) - button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) end if status_light_enabled_present then table.insert(status_led_component_capabilities, capabilities.switch.ID) @@ -147,6 +145,10 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr if camera_utils.optional_capabilities_list_changed(optional_supported_component_capabilities, device.profile.components) then device:try_update_metadata({profile = "camera", optional_component_capabilities = optional_supported_component_capabilities}) + if #doorbell_endpoints > 0 then + CameraDeviceConfiguration.update_doorbell_component_map(device, doorbell_endpoints[1]) + button_cfg.configure_buttons(device, device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH})) + end end end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index f08e757553..a93e757c16 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -120,28 +120,49 @@ function CameraUtils.profile_changed(synced_components, prev_components) return false end -function CameraUtils.optional_capabilities_list_changed(optional_capabilities, prev_component_list) - local prev_optional_capabilities = {} - for idx, comp in pairs(prev_component_list or {}) do - local cap_list = {} - for _, capability in pairs(comp.capabilities or {}) do - table.insert(cap_list, capability.id) +function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list) + local previous_capability_map = {} + local component_sizes = {} + + local previous_component_count = 0 + for component_name, component in pairs(previous_component_capability_list or {}) do + previous_capability_map[component_name] = {} + component_sizes[component_name] = 0 + for _, capability in pairs(component.capabilities or {}) do + if capability.id ~= "firmwareUpdate" and capability.id ~= "refresh" then + previous_capability_map[component_name][capability.id] = true + component_sizes[component_name] = component_sizes[component_name] + 1 + end end - table.insert(prev_optional_capabilities, {idx, cap_list}) - end - if #optional_capabilities ~= #prev_optional_capabilities then - return true + previous_component_count = previous_component_count + 1 end - for _, capability in pairs(optional_capabilities or {}) do - if not switch_utils.tbl_contains(prev_optional_capabilities, capability) then + + local number_of_components_counted = 0 + for _, new_component_capabilities in pairs(new_component_capability_list or {}) do + local component_name = new_component_capabilities[1] + local capability_list = new_component_capabilities[2] + + number_of_components_counted = number_of_components_counted + 1 + + if previous_capability_map[component_name] == nil then return true end - end - for _, capability in pairs(prev_optional_capabilities or {}) do - if not switch_utils.tbl_contains(optional_capabilities, capability) then + + for _, capability in ipairs(capability_list) do + if previous_capability_map[component_name][capability] == nil then + return true + end + end + + if #capability_list ~= component_sizes[component_name] then return true end end + + if number_of_components_counted ~= previous_component_count then + return true + end + return false 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 a327c39c31..1028197590 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -5,6 +5,7 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" test.disable_startup_messages() @@ -284,7 +285,6 @@ local expected_metadata = { } local function update_device_profile() - local uint32 = require "st.matter.data_types.Uint32" test.socket.matter:__queue_receive({ mock_device.id, clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { @@ -292,8 +292,9 @@ local function update_device_profile() uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) }) }) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) mock_device:expect_metadata_update(expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.wait_for_events() local updated_device_profile = t_utils.get_profile_definition( "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} ) @@ -1830,5 +1831,59 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Camera profile should not update for an unchanged Status Light AttributeList report", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + end +) + +test.register_coroutine_test( + "Camera profile should update for a changed Status Light AttributeList report", + function() + update_device_profile() + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) + }) + }) + local expected_metadata = { + optional_component_capabilities = { + { "main", + { "videoCapture2", "cameraViewportSettings", "localMediaStorage", "audioRecording", "cameraPrivacyMode", + "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", + "webrtc", "motionSensor", "sounds", } + }, + { "statusLed", + { "switch" } -- only switch capability remains + }, + { "speaker", + { "audioMute", "audioVolume" } + }, + { "microphone", + { "audioMute", "audioVolume" } + }, + { "doorbell", + { "button" } + } + }, + profile = "camera" + } + mock_device:expect_metadata_update(expected_metadata) + test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) + test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) + end +) + -- run the tests test.run_registered_tests() From cdf4d494305f9c6a569457c367bc1abb1509aa7b Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 12 Jan 2026 14:48:40 -0800 Subject: [PATCH 370/449] WWSTCERT-9480 Sensereo MSC-1 --- drivers/SmartThings/matter-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/fingerprints.yml b/drivers/SmartThings/matter-sensor/fingerprints.yml index 0db00e11d6..4295c87a93 100644 --- a/drivers/SmartThings/matter-sensor/fingerprints.yml +++ b/drivers/SmartThings/matter-sensor/fingerprints.yml @@ -215,6 +215,11 @@ matterManufacturer: vendorId: 0x1596 productId: 0x0001 deviceProfileName: smoke-battery + - id: "5526/2" + deviceLabel: MSC-1 + vendorId: 0x1596 + productId: 0x0002 + deviceProfileName: smoke-co-comeas-battery # Siterwell - id: "4736/847" deviceLabel: Siterwell Door Window Sensor From 689a6a50f9802ade090bf4030666c7a75ed6a5e3 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 12 Jan 2026 14:59:35 -0800 Subject: [PATCH 371/449] WWSTCERT-9605 Kwikset Aura Reach --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 2 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 6c7a223caf..71a3acd65f 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -62,6 +62,11 @@ matterManufacturer: vendorId: 0x1421 productId: 0x0042 deviceProfileName: lock-user-pin-battery + - id: "5153/129" + deviceLabel: Kwikset Aura Reach + vendorId: 0x1421 + productId: 0x0081 + deviceProfileName: lock-user-pin-battery #Level - id: "4767/1" deviceLabel: Level Lock Plus (Matter) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 1d59b1ab14..3a2b78234b 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -85,6 +85,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x158B, 0x0001}, -- Deasino, DS-MT01 {0x10E1, 0x2002}, -- VDA {0x1421, 0x0042}, -- Kwikset Halo Select Plus + {0x1421, 0x0081}, -- Kwikset Aura Reach } local battery_support = { From 2a71e6a592d936e2a6de4d7113b95f32e0effe50 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 12 Jan 2026 15:11:31 -0800 Subject: [PATCH 372/449] WWSTCERT-9450 SONOFF Zigbee Temperature and Humidity Sensor --- drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index 68359710fe..e5ad9e571a 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -79,7 +79,7 @@ zigbeeManufacturer: model: TH01 deviceProfileName: humidity-temp-battery - id: eWeLink/SNZB-02P - deviceLabel: eWeLink Multipurpose Sensor + deviceLabel: SONOFF Zigbee Temperature and Humidity Sensor manufacturer: eWeLink model: SNZB-02P deviceProfileName: humidity-temp-battery From 40c934686454de1a2376e8af40ad95c6e186a988 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 13 Jan 2026 14:57:41 -0800 Subject: [PATCH 373/449] WWSTCERT-9799 Eve Thermostat (Europe) --- drivers/SmartThings/matter-thermostat/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 6cd56d3a24..30e99caadb 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -33,6 +33,11 @@ matterManufacturer: vendorId: 0x130A productId: 0x007D deviceProfileName: thermostat-heating-only-nostate + - id: "4874/127" + deviceLabel: Eve Thermostat (Europe) + vendorId: 0x130A + productId: 0x007F + deviceProfileName: thermostat-heating-only-nostate-nobattery #Habi - id: "5415/1" deviceLabel: habi Matter Wireless Room Thermostat From 05cce9cc7f3829677e93bee14faceac41674ca9c Mon Sep 17 00:00:00 2001 From: Taejun Park Date: Mon, 15 Dec 2025 20:33:41 +0900 Subject: [PATCH 374/449] Add the ReadAttrbitue or refresh code for the initial state --- .../zigbee-illuminance-sensor/src/init.lua | 8 +++ .../src/test/test_illuminance_sensor.lua | 2 + .../src/test/test_multi_switch_no_master.lua | 3 + ...metering_plug_power_consumption_report.lua | 69 ++++++++++++++++++- .../init.lua | 1 + .../zigbee-window-treatment/src/axis/init.lua | 1 + .../src/feibit/init.lua | 1 + .../test_zigbee_window_treatment_axis.lua | 2 + .../test_zigbee_window_treatment_feibit.lua | 2 + 9 files changed, 88 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 45a3ade62b..635503c232 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -16,6 +16,11 @@ local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" +local do_configure = function(self, device) + device:configure() + device:refresh() +end + local zigbee_illuminance_driver = { supported_capabilities = { capabilities.illuminanceMeasurement, @@ -24,6 +29,9 @@ local zigbee_illuminance_driver = { sub_drivers = { require("aqara") }, + lifecycle_handlers = { + doConfigure = do_configure, + }, health_check = false, } 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 02c3c63a55..1987964075 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 @@ -107,6 +107,8 @@ test.register_coroutine_test( zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) } ) + test.socket.zigbee:__expect_send({ mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end ) 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 6603a0831c..0f00d47fdb 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 @@ -469,6 +469,9 @@ test.register_coroutine_test( 3 ):to_endpoint(3) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device):to_endpoint(2) }) + test.socket.zigbee:__expect_send({ mock_base_device.id, OnOff.attributes.OnOff:read(mock_base_device):to_endpoint(3) }) mock_base_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) 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 4514a90cbb..d701bd89aa 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 @@ -6,6 +6,8 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement +local OnOff = clusters.OnOff local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" @@ -17,7 +19,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( id = 1, manufacturer = "DAWON_DNS", model = "PM-B430-ZB", - server_clusters = {} + server_clusters = { 0x0000, 0x0002, 0x0003, 0x0006, 0x0702, 0x0B04 } } } } @@ -81,4 +83,69 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Configure should configure all necessary attributes and refresh device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOff.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 300, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + } + ) + + 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, ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua index 17dc0ee2bd..53a7e1eb99 100644 --- a/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/zigbee-metering-plug-power-consumption-report/init.lua @@ -24,6 +24,7 @@ end local do_configure = function(self, device) device:configure() + device:refresh() end local device_init = function(self, device) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua index a47de06df7..612dd450a5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua @@ -110,6 +110,7 @@ local do_configure = function(self, device) 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:refresh() end local device_added = function(self, device) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua index f33bf81cbd..e0fd17219e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua @@ -61,6 +61,7 @@ end local do_configure = function(self, device) device:send(device_management.build_bind_request(device, Level.ID, self.environment_info.hub_zigbee_eui)) device:send(Level.attributes.CurrentLevel:configure_reporting(device, 1, 3600, 1)) + device:refresh() end local feibit_handler = { 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 5068f889a5..f3564ff79f 100755 --- 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 @@ -637,6 +637,8 @@ test.register_coroutine_test( zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) }) end ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua index c65d38281a..aa0480eb2a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua @@ -20,6 +20,7 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local Level = clusters.Level +local WindowCovering = clusters.WindowCovering local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("window-treatment-profile.yml"), @@ -396,6 +397,7 @@ test.register_coroutine_test( zigbee_test_utils.mock_hub_eui, Level.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) From 8841dcaced89c52b06fd84d20a9cfd51c58d9fca Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 14 Jan 2026 15:05:52 -0600 Subject: [PATCH 375/449] Matter Camera: Add vision.clipAnalysis capability Add vision.clipAnalysis to the Matter Camera profile. --- drivers/SmartThings/matter-switch/profiles/camera.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/SmartThings/matter-switch/profiles/camera.yml b/drivers/SmartThings/matter-switch/profiles/camera.yml index 7a46099cf6..7f62319984 100644 --- a/drivers/SmartThings/matter-switch/profiles/camera.yml +++ b/drivers/SmartThings/matter-switch/profiles/camera.yml @@ -47,6 +47,9 @@ components: - id: motionSensor version: 1 optional: true + - id: vision.clipAnalysis + version: 1 + optional: true - id: firmwareUpdate version: 1 - id: refresh From a218cda82359844bee48039fa908b69f4eeef58e Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Thu, 15 Jan 2026 14:30:47 -0600 Subject: [PATCH 376/449] run_driver_tests.py: add option to output html coverage report This adds an optional argument to the run_driver_tests.py script, which when passed generates HTML coverage reports for the files specified by the --coverage argument. These are handy for opening up in a browser or previewing in an IDE to help spot gaps in line coverage. --- .gitignore | 2 ++ tools/run_driver_tests.py | 34 +++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index c5bbdc6f99..72da442d81 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ lua_libs-api_* *.out tools/test_output/* tools/coverage_output/* +tools/coverage_output_html/ +tools/__pycache__/ .DS_Store .venv/ diff --git a/tools/run_driver_tests.py b/tools/run_driver_tests.py index c797ef5320..e3e58f2154 100755 --- a/tools/run_driver_tests.py +++ b/tools/run_driver_tests.py @@ -7,6 +7,7 @@ import argparse from pathlib import Path import junit_xml +import shutil VERBOSITY_TOTALS_ONLY = 0 VERBOSITY_TEST_STATUS_ONLY = 1 @@ -14,7 +15,7 @@ VERBOSITY_ALL_TEST_LOGS = 3 DRIVER_DIR = Path(os.path.abspath(__file__)).parents[1].joinpath("drivers") -LUACOV_CONFIG = DRIVER_DIR.parent.joinpath(".circleci", "config.luacov") +LUACOV_CONFIG = DRIVER_DIR.parent.joinpath("tools", "config.luacov") def find_affected_tests(working_dir, changed_files): affected_tests = [] @@ -29,13 +30,14 @@ def find_affected_tests(working_dir, changed_files): affected_tests = set(affected_tests) return affected_tests -def run_tests(verbosity_level, filter, junit, coverage_files): +def run_tests(verbosity_level, filter, junit, coverage_files, html): owd = os.getcwd() coverage_files = find_affected_tests(owd, coverage_files) failure_files = defaultdict(list) ts = [] total_tests = 0 total_passes = 0 + drivers_needing_html = {} 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 @@ -137,7 +139,29 @@ def run_tests(verbosity_level, filter, junit, coverage_files): test_suite.test_cases = test_cases ts.append(test_suite) if test_file in coverage_files: - subprocess.run("luacov -c={}".format(LUACOV_CONFIG), shell=True) + if html: + driver_name = test_file.parts[-4] + src_path = test_file.parents[1] + drivers_needing_html[driver_name] = src_path + else: + subprocess.run("luacov -c={}".format(LUACOV_CONFIG), shell=True) + + if drivers_needing_html: + coverage_html_dir = DRIVER_DIR.parent.joinpath("tools/coverage_output_html") + try: + os.mkdir(coverage_html_dir) + except FileExistsError: + pass + + for driver_name, src_path in drivers_needing_html.items(): + os.chdir(src_path) + subprocess.run("luacov -c {} --reporter html".format(LUACOV_CONFIG), shell=True) + html_source = Path("luacov.report.out") + html_dest = coverage_html_dir.joinpath(driver_name+"_luacov.report.html") + if html_source.exists(): + shutil.copy(html_source, html_dest) + else: + print(f"Warning: HTML coverage file for {driver_name} not found") total_test_info = "Total unit tests passes: {}/{}".format(total_passes, total_tests) print("#" * len(total_test_info)) @@ -165,6 +189,7 @@ def run_tests(verbosity_level, filter, junit, coverage_files): parser.add_argument("--filter", "-f", type=str, nargs="?", help="only run tests containing the filter value in the path") parser.add_argument("--junit", "-j", type=str, nargs="?", help="output test results in JUnit XML to the specified file") parser.add_argument("--coverage", "-c", nargs="*", help="run code tests with coverage (luacov must be installed) OPTIONAL: restrict files to run coverage tests for") + parser.add_argument("--html", action="store_true", help="Generate HTML coverage reports for the files specified by the coverage argument") args = parser.parse_args() verbosity_level = 0 if args.verbose: @@ -173,5 +198,4 @@ def run_tests(verbosity_level, filter, junit, coverage_files): verbosity_level = 2 elif args.superextraverbose: verbosity_level = 3 - run_tests(verbosity_level, args.filter, args.junit, args.coverage) - + run_tests(verbosity_level, args.filter, args.junit, args.coverage, args.html) From 9ba659d064eef34bcd87a23cc9fab2d63ed5f09a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 5 Dec 2025 11:44:03 -0800 Subject: [PATCH 377/449] WWSTCERT-9289 eufy FamiLock E40 --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 2 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 6c7a223caf..52734b703b 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -56,6 +56,11 @@ matterManufacturer: vendorId: 0x1533 productId: 0x0016 deviceProfileName: lock-user-pin-battery + - id: 5427/20 + deviceLabel: eufy FamiLock E40 + vendorId: 0x1533 + productId: 0x0014 + deviceProfileName: lock-user-pin-battery #Kwikset - id: "5153/66" deviceLabel: Kwikset Halo Select Plus diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 1d59b1ab14..7e8c35ec9e 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -76,6 +76,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x1533, 0x0011}, -- eufy, FamiLock E34 {0x1533, 0x0012}, -- eufy, FamiLock E35 {0x1533, 0x0016}, -- eufy, FamiLock E32 + {0x1533, 0x0014}, -- eufy, FamiLock E40 {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro {0x135D, 0x00B2}, -- Nuki, Smart Lock {0x135D, 0x00C1}, -- Nuki, Smart Lock From 3cae7e15e2972411289e09c60a8fe64422163b9a Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 15 Jan 2026 13:43:28 -0800 Subject: [PATCH 378/449] WWSTCERT-9960 HooRii Window Covering --- .../SmartThings/matter-window-covering/fingerprints.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 532ba1f304..0de1e91af8 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -36,6 +36,12 @@ matterManufacturer: vendorId: 0x153B productId: 0x3801 deviceProfileName: window-covering-tilt +# HooRii + - id: "4945/61171" + deviceLabel: HooRii Window Covering + vendorId: 0x1351 + productId: 0xEEF3 + deviceProfileName: window-covering-battery # Meross - id: "4933/61453" deviceLabel: Smart Wi-Fi Roller Shutter Timer @@ -176,7 +182,7 @@ matterManufacturer: deviceLabel: WISTAR WSCMXF-LED Smart Vertical Blind Motor vendorId: 0x1457 productId: 0x0018 - deviceProfileName: window-covering-tilt + deviceProfileName: window-covering-tilt - id: "5207/20" deviceLabel: WISTAR WSCMQ Smart Curtain Motor vendorId: 0x1457 From e0805bafd3271ced84f5d5ef4470cd8485c67ede Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:49:26 +0900 Subject: [PATCH 379/449] Add vid/pid of the Aqara U400 to new-matter-lock (#2709) Signed-off-by: Hunsup Jung --- drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 1d59b1ab14..9d3f0357dc 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -64,6 +64,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2802}, -- AQARA, U200 {0x115f, 0x2801}, -- AQARA, U300 {0x115f, 0x2807}, -- AQARA, U200 Lite + {0x115f, 0x2804}, -- AQARA, U400 {0x147F, 0x0001}, -- U-tec {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 From 17c337edc7d0e52cb78edc78edeb2310a1eb7bd9 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Fri, 16 Jan 2026 15:25:30 -0800 Subject: [PATCH 380/449] Osram switches (#2697) * Osram switches WWSTCERT-9564 WWSTCERT-9561 WWSTCERT-9558 WWSTCERT-9555 WWSTCERT-9552 WWSTCERT-9549 WWSTCERT-9546 WWSTCERT-9543 WWSTCERT-9540 WWSTCERT-9537 WWSTCERT-9534 WWSTCERT-9531 WWSTCERT-9528 WWSTCERT-9525 WWSTCERT-9522 WWSTCERT-9519 WWSTCERT-9516 WWSTCERT-9513 WWSTCERT-9510 WWSTCERT-9507 WWSTCERT-9504 WWSTCERT-9501 * WWSTCERT-9579 WWSTCERT-9576 WWSTCERT-9573 WWSTCERT-9570 WWSTCERT-9567 * WWSTCERT-9779 WWSTCERT-9776 WWSTCERT-9773 WWSTCERT-9770 WWSTCERT-9767 WWSTCERT-9764 WWSTCERT-9761 WWSTCERT-9758 WWSTCERT-9755 WWSTCERT-9752 WWSTCERT-9749 WWSTCERT-9746 WWSTCERT-9743 WWSTCERT-9740 WWSTCERT-9737 WWSTCERT-9734 WWSTCERT-9731 WWSTCERT-9728 WWSTCERT-9725 WWSTCERT-9722 WWSTCERT-9719 WWSTCERT-9716 WWSTCERT-9713 WWSTCERT-9710 WWSTCERT-9707 WWSTCERT-9704 WWSTCERT-9701 WWSTCERT-9698 WWSTCERT-9695 WWSTCERT-9692 WWSTCERT-9689 WWSTCERT-9686 WWSTCERT-9683 WWSTCERT-9680 WWSTCERT-9677 WWSTCERT-9674 WWSTCERT-9671 WWSTCERT-9668 WWSTCERT-9665 WWSTCERT-9662 WWSTCERT-9659 WWSTCERT-9656 WWSTCERT-9653 WWSTCERT-9650 WWSTCERT-9647 WWSTCERT-9644 WWSTCERT-9641 WWSTCERT-9638 WWSTCERT-9635 WWSTCERT-9632 WWSTCERT-9629 WWSTCERT-9626 WWSTCERT-9623 WWSTCERT-9620 --- .../matter-switch/fingerprints.yml | 407 +++++++++++++++++- 1 file changed, 406 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 9a2cbbaa89..32a4df6694 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -1411,6 +1411,411 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0A7E deviceProfileName: light-level + - id: "4489/2725" + deviceLabel: SMART MAT PAR16 RGBW GU10 + vendorId: 0x1189 + productId: 0x0AA5 + deviceProfileName: light-color-level + - id: "4489/2766" + deviceLabel: SMART MAT B40 RGBW 827 FR E14 + vendorId: 0x1189 + productId: 0x0ACE + deviceProfileName: light-color-level + - id: "4489/2765" + deviceLabel: SMART MAT P40 RGBW 827 FR E14 + vendorId: 0x1189 + productId: 0x0ACD + deviceProfileName: light-color-level + - id: "4489/2764" + deviceLabel: SMART MAT G95 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0ACC + deviceProfileName: light-color-level + - id: "4489/2774" + deviceLabel: SMART MAT A40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD6 + deviceProfileName: light-color-level + - id: "4489/2775" + deviceLabel: SMART MAT E40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD7 + deviceProfileName: light-color-level + - id: "4489/2776" + deviceLabel: SMART MAT G40 FIL RGBW 827 E27 + vendorId: 0x1189 + productId: 0x0AD8 + deviceProfileName: light-color-level + - id: "4489/2837" + deviceLabel: SMART MAT A75 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0B15 + deviceProfileName: light-color-level + - id: "4489/2713" + deviceLabel: SMART MAT A60 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0A99 + deviceProfileName: light-level-colorTemperature + - id: "4489/2711" + deviceLabel: SMART MAT A100 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0A97 + deviceProfileName: light-level-colorTemperature + - id: "4489/2716" + deviceLabel: SMART MAT P40 TW 827 FR E14 + vendorId: 0x1189 + productId: 0x0A9C + deviceProfileName: light-level-colorTemperature + - id: "4489/2726" + deviceLabel: SMART MAT PAR16 TW 827 GU10 + vendorId: 0x1189 + productId: 0x0AA6 + deviceProfileName: light-level-colorTemperature + - id: "4489/2678" + deviceLabel: SMART MAT E60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A76 + deviceProfileName: light-level + - id: "4489/2676" + deviceLabel: SMART MAT G60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A74 + deviceProfileName: light-level + - id: "4489/2687" + deviceLabel: SMART MAT B40 DIM FIL 827 E14 + vendorId: 0x1189 + productId: 0x0A7F + deviceProfileName: light-level + - id: "4489/2685" + deviceLabel: SMART MAT E53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7D + deviceProfileName: light-level + - id: "4489/2684" + deviceLabel: SMART MAT G53 DIM FILGD 824 E27 + vendorId: 0x1189 + productId: 0x0A7C + deviceProfileName: light-level + - id: "4489/2681" + deviceLabel: SMART MAT A60 DIM FIL 827 E27 + vendorId: 0x1189 + productId: 0x0A79 + deviceProfileName: light-level + - id: "4489/2682" + deviceLabel: SMART MAT P40 DIM FIL 827 E14 + vendorId: 0x1189 + productId: 0x0A7A + deviceProfileName: light-level + - id: "4489/2727" + deviceLabel: SMART MAT PAR16 DIM GU10 + vendorId: 0x1189 + productId: 0x0AA7 + deviceProfileName: light-level + - id: "4489/2712" + deviceLabel: SMART MAT G95 100 TW 827 E27 + vendorId: 0x1189 + productId: 0x0A98 + deviceProfileName: light-level-colorTemperature + - id: "4489/2836" + deviceLabel: SMART MAT A75 TW 827 FR E27 + vendorId: 0x1189 + productId: 0x0B14 + deviceProfileName: light-level-colorTemperature + - id: "4489/2514" + deviceLabel: SMART MAT ORBIS DL 200 TW WT + vendorId: 0x1189 + productId: 0x09D2 + deviceProfileName: light-color-level + - id: "4489/2518" + deviceLabel: SMART MAT ORBIS DL 400 TW WT + vendorId: 0x1189 + productId: 0x09D6 + deviceProfileName: light-color-level + - id: "4489/2519" + deviceLabel: SMART MAT ORBIS DL SQ400 TW WT + vendorId: 0x1189 + productId: 0x09D7 + deviceProfileName: light-color-level + - id: "4489/2763" + deviceLabel: SMART MAT A60 RGBW 827 FR B22D + vendorId: 0x1189 + productId: 0x0ACB + deviceProfileName: light-color-level + - id: "4489/2756" + deviceLabel: SMART MAT A100 RGBW 827 FR E27 + vendorId: 0x1189 + productId: 0x0AC4 + deviceProfileName: light-color-level + - id: "4489/3027" + deviceLabel: SMART MAT DECORWALLSWAN300X150TW + vendorId: 0x1189 + productId: 0x0BD3 + deviceProfileName: light-level-colorTemperature + - id: "4489/2994" + deviceLabel: SMART MAT PLANON FL 120X30 TW + vendorId: 0x1189 + productId: 0x0BB2 + deviceProfileName: light-level-colorTemperature + - id: "4489/3032" + deviceLabel: SMART WIFI MAT BATH W400 IP44 TW + vendorId: 0x1189 + productId: 0x0BD8 + deviceProfileName: light-level-colorTemperature + - id: "4489/3026" + deviceLabel: SMART WIFI MAT WAVE300X300IP44TW + vendorId: 0x1189 + productId: 0x0BD2 + deviceProfileName: light-level-colorTemperature + - id: "4489/2957" + deviceLabel: SMART WIFI MAT TRACKSP OSAKA TW + vendorId: 0x1189 + productId: 0x0B8D + deviceProfileName: light-level-colorTemperature + - id: "4489/3025" + deviceLabel: SMART WIFI MAT CYLINDER 450 TW + vendorId: 0x1189 + productId: 0x0BD1 + deviceProfileName: light-level-colorTemperature + - id: "4489/2969" + deviceLabel: SMART MAT PLANON PLUS 120X30TW + vendorId: 0x1189 + productId: 0x0B99 + deviceProfileName: light-level-colorTemperature + - id: "4489/3028" + deviceLabel: SMART MAT DISC 400 IP44 TW + vendorId: 0x1189 + productId: 0x0BD4 + deviceProfileName: light-level-colorTemperature + - id: "4489/2971" + deviceLabel: SMART MAT PLANON FL 80X10TW + vendorId: 0x1189 + productId: 0x0B9B + deviceProfileName: light-level-colorTemperature + - id: "4489/3014" + deviceLabel: SMART MAT PLANONFLSPARK45X45TW + vendorId: 0x1189 + productId: 0x0BC6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3023" + deviceLabel: SMART WIFI MAT MAGNET 600X300 TW + vendorId: 0x1189 + productId: 0x0BCF + deviceProfileName: light-level-colorTemperature + - id: "4489/3021" + deviceLabel: SMART WIFI MAT AQUA280X160IP44TW + vendorId: 0x1189 + productId: 0x0BCD + deviceProfileName: light-level-colorTemperature + - id: "4489/3035" + deviceLabel: SMART WIFI MAT DUPLO 300 IP44 TW + vendorId: 0x1189 + productId: 0x0BDB + deviceProfileName: light-level-colorTemperature + - id: "4489/3024" + deviceLabel: SMART WIFI MAT DUPLO 580 IP44 TW + vendorId: 0x1189 + productId: 0x0BD0 + deviceProfileName: light-level-colorTemperature + - id: "4489/2998" + deviceLabel: SMART WIFI MAT DL SLIM 120 TW + vendorId: 0x1189 + productId: 0x0BB6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3020" + deviceLabel: SMART MATDECORWALLTWIST230X127TW + vendorId: 0x1189 + productId: 0x0BCC + deviceProfileName: light-level-colorTemperature + - id: "4489/3049" + deviceLabel: SMART WIFI MAT TRACKSP CIRCLE TW + vendorId: 0x1189 + productId: 0x0BE9 + deviceProfileName: light-level-colorTemperature + - id: "4489/3029" + deviceLabel: SMART WIFI MAT DUPLO 450 IP44 TW + vendorId: 0x1189 + productId: 0x0BD5 + deviceProfileName: light-level-colorTemperature + - id: "4489/2991" + deviceLabel: SMART MAT PLANON PLUS 60X60 TW + vendorId: 0x1189 + productId: 0x0BAF + deviceProfileName: light-level-colorTemperature + - id: "4489/2985" + deviceLabel: SMART MAT PLANON PLUS 30X30 TW + vendorId: 0x1189 + productId: 0x0BA9 + deviceProfileName: light-level-colorTemperature + - id: "4489/3033" + deviceLabel: SMART WIFI MAT BATH W300 IP44 TW + vendorId: 0x1189 + productId: 0x0BD9 + deviceProfileName: light-level-colorTemperature + - id: "4489/2999" + deviceLabel: SMART WIFI MAT DL SLIM 225 TW + vendorId: 0x1189 + productId: 0x0BB7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3037" + deviceLabel: SMART WIFI MAT MAGNET 300X300 TW + vendorId: 0x1189 + productId: 0x0BDD + deviceProfileName: light-level-colorTemperature + - id: "4489/2990" + deviceLabel: SMART MAT PLANON PLUS 60X30 TW + vendorId: 0x1189 + productId: 0x0BAE + deviceProfileName: light-level-colorTemperature + - id: "4489/3047" + deviceLabel: SMART WIFI MAT TRACKSP OSAKA TW + vendorId: 0x1189 + productId: 0x0BE7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3031" + deviceLabel: SMART WIFI MAT MAGNET 600X300 TW + vendorId: 0x1189 + productId: 0x0BD7 + deviceProfileName: light-level-colorTemperature + - id: "4489/3034" + deviceLabel: SMART WIFI MAT DISC 300 IP44 TW + vendorId: 0x1189 + productId: 0x0BDA + deviceProfileName: light-level-colorTemperature + - id: "4489/3030" + deviceLabel: SMART WIFI MAT MAGNET 450X450 TW + vendorId: 0x1189 + productId: 0x0BD6 + deviceProfileName: light-level-colorTemperature + - id: "4489/3022" + deviceLabel: SMART WIFI MAT AQUA200X200IP44TW + vendorId: 0x1189 + productId: 0x0BCE + deviceProfileName: light-level-colorTemperature + - id: "4489/2993" + deviceLabel: SMART MAT PLANON FL 30X30 TW + vendorId: 0x1189 + productId: 0x0BB1 + deviceProfileName: light-level-colorTemperature + - id: "4489/3048" + deviceLabel: SMART WIFI MAT TRACKSP CIRCLE TW + vendorId: 0x1189 + productId: 0x0BE8 + deviceProfileName: light-level-colorTemperature + - id: "4489/2997" + deviceLabel: SMART WIFI MAT DL SLIM 85 TW WT + vendorId: 0x1189 + productId: 0x0BB5 + deviceProfileName: light-level-colorTemperature + - id: "4489/2949" + deviceLabel: SMART WIFI MAT SPIRAL 500 TW + vendorId: 0x1189 + productId: 0x0B85 + deviceProfileName: light-level-colorTemperature + - id: "4489/3036" + deviceLabel: SMART WFMTDECORWALLSWAN200X200TW + vendorId: 0x1189 + productId: 0x0BDC + deviceProfileName: light-level-colorTemperature + - id: "4489/2995" + deviceLabel: SMART MAT PLANON FL 120X10 TW + vendorId: 0x1189 + productId: 0x0BB3 + deviceProfileName: light-level-colorTemperature + - id: "4489/2987" + deviceLabel: SMART MAT PLANON PLUS 45X45 TW + vendorId: 0x1189 + productId: 0x0BAB + deviceProfileName: light-level-colorTemperature + - id: "4489/3018" + deviceLabel: SMART WFMTDECORWALLWOOD210X110TW + vendorId: 0x1189 + productId: 0x0BCA + deviceProfileName: light-level-colorTemperature + - id: "4489/3019" + deviceLabel: SMART MATDECORWALLTWIST230X127TW + vendorId: 0x1189 + productId: 0x0BCB + deviceProfileName: light-level-colorTemperature + - id: "4489/3055" + deviceLabel: SMART WIFI MAT FLOOD 50W RGBW + vendorId: 0x1189 + productId: 0x0BEF + deviceProfileName: light-color-level + - id: "4489/2970" + deviceLabel: SMART MAT PLANON PLUS60X60RGBTW + vendorId: 0x1189 + productId: 0x0B9A + deviceProfileName: light-color-level + - id: "4489/3057" + deviceLabel: SMART WIFI MAT FLOOD 20W RGBW + vendorId: 0x1189 + productId: 0x0BF1 + deviceProfileName: light-color-level + - id: "4489/2973" + deviceLabel: SMART MAT PLANON PLUS45X45RGBW + vendorId: 0x1189 + productId: 0x0B9D + deviceProfileName: light-color-level + - id: "4489/2974" + deviceLabel: SMART MAT PLANON PLUS60X30RGBW + vendorId: 0x1189 + productId: 0x0B9E + deviceProfileName: light-color-level + - id: "4489/3059" + deviceLabel: SMART WIFI MAT CLARIA 490RGBTW + vendorId: 0x1189 + productId: 0x0BF3 + deviceProfileName: light-color-level + - id: "4489/3058" + deviceLabel: SMART WIFI MAT FLOOD 30W RGBW + vendorId: 0x1189 + productId: 0x0BF2 + deviceProfileName: light-color-level + - id: "4489/2972" + deviceLabel: SMART MAT PLANON PLUS60X60RGBW + vendorId: 0x1189 + productId: 0x0B9C + deviceProfileName: light-color-level + - id: "4489/2984" + deviceLabel: SMART MAT PLANON PLUS120X30RGBW + vendorId: 0x1189 + productId: 0x0BA8 + deviceProfileName: light-color-level + - id: "4489/3001" + deviceLabel: SMART WIFI MAT SP170 110DEGRGBTW + vendorId: 0x1189 + productId: 0x0BB9 + deviceProfileName: light-color-level + - id: "4489/2992" + deviceLabel: SMART MAT PLANON PLUS30X30RGBW + vendorId: 0x1189 + productId: 0x0BB0 + deviceProfileName: light-color-level + - id: "4489/3056" + deviceLabel: SMART WIFI MAT FLOOD 10W RGBW + vendorId: 0x1189 + productId: 0x0BF0 + deviceProfileName: light-color-level + - id: "4489/3016" + deviceLabel: SMART OUTD MAT BEAMADJWALL RGBTW + vendorId: 0x1189 + productId: 0x0BC8 + deviceProfileName: light-color-level + - id: "4489/3000" + deviceLabel: SMART WIFI MAT SP 86 100DEGRGBTW + vendorId: 0x1189 + productId: 0x0BB8 + deviceProfileName: light-color-level + - id: "4489/3003" + deviceLabel: SMART WIFI MAT STEA485RD RGBTW + vendorId: 0x1189 + productId: 0x0BBB + deviceProfileName: light-color-level + - id: "4489/2963" + deviceLabel: SMART WIFI MAT FLOOD 100W RGBW + vendorId: 0x1189 + productId: 0x0B93 + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 @@ -3604,7 +4009,7 @@ matterGeneric: deviceTypes: - id: 0x002B # Fan - id: 0x0100 # OnOff Light - deviceProfileName: fan-modular + deviceProfileName: fan-modular - id: "matter/dimmable/fan/light" deviceLabel: Matter Dimmable Fan Light deviceTypes: From 15b4d7bce5f234a3015533f7c1c90e1a59221320 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 13 Jan 2026 16:15:34 -0800 Subject: [PATCH 381/449] WWSTCERT-9840 Aqara U200 US --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua | 1 + 2 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 71a3acd65f..dba88fb4dd 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -15,6 +15,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2807 deviceProfileName: lock-battery + - id: "4447/10346" + deviceLabel: Aqara Smart Lock U200 US + vendorId: 0x115F + productId: 0x286A + deviceProfileName: lock-user-pin #Eufy - id: "5427/1" deviceLabel: eufy Smart Lock E31 diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 725f31bdab..bd8316cb01 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -65,6 +65,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x2801}, -- AQARA, U300 {0x115f, 0x2807}, -- AQARA, U200 Lite {0x115f, 0x2804}, -- AQARA, U400 + {0x115f, 0x286A}, -- AQARA, U200 US {0x147F, 0x0001}, -- U-tec {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 From 2192441df053cb9bd25e96c81e50e84f17c053dc Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Tue, 20 Jan 2026 13:27:16 -0600 Subject: [PATCH 382/449] Revert "Exclude off from fanMode capability for Thermostats (#2673)" (#2715) This reverts commit b8b70686af6bfb80cc351a31c5e6d59653a88d72. --- .../attribute_handlers.lua | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) 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 fd5dca06a4..78f5ee3bf2 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -48,11 +48,7 @@ function AttributeHandlers.system_mode_handler(driver, device, ib, response) return end - local supported_modes = device:get_latest_state( - device:endpoint_to_component(ib.endpoint_id), - capabilities.thermostatMode.ID, - capabilities.thermostatMode.supportedThermostatModes.NAME - ) or {} + local supported_modes = device:get_latest_state(device:endpoint_to_component(ib.endpoint_id), capabilities.thermostatMode.ID, capabilities.thermostatMode.supportedThermostatModes.NAME) or {} -- check that the given mode was in the supported modes list if thermostat_utils.tbl_contains(supported_modes, fields.THERMOSTAT_MODE_MAP[ib.data.value].NAME) then device:emit_event_for_endpoint(ib.endpoint_id, fields.THERMOSTAT_MODE_MAP[ib.data.value]()) @@ -329,19 +325,19 @@ function AttributeHandlers.fan_mode_handler(driver, device, ib, response) end function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, response) - local supported_fan_modes, supported_fan_modes_attribute + local supportedFanModes, supported_fan_modes_attribute if ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH then - supported_fan_modes = { "off", "low", "medium", "high" } + supportedFanModes = { "off", "low", "medium", "high" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH then - supported_fan_modes = { "off", "low", "high" } + supportedFanModes = { "off", "low", "high" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO then - supported_fan_modes = { "off", "low", "medium", "high", "auto" } + supportedFanModes = { "off", "low", "medium", "high", "auto" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_LOW_HIGH_AUTO then - supported_fan_modes = { "off", "low", "high", "auto" } + supportedFanModes = { "off", "low", "high", "auto" } elseif ib.data.value == clusters.FanControl.attributes.FanModeSequence.OFF_HIGH_AUTO then - supported_fan_modes = { "off", "high", "auto" } + supportedFanModes = { "off", "high", "auto" } else - supported_fan_modes = { "off", "high" } + supportedFanModes = { "off", "high" } end if device:supports_capability_by_id(capabilities.airPurifierFanMode.ID) then @@ -353,22 +349,16 @@ function AttributeHandlers.fan_mode_sequence_handler(driver, device, ib, respons -- Our thermostat fan mode control is not granular enough to handle all of the supported modes if ib.data.value >= clusters.FanControl.attributes.FanModeSequence.OFF_LOW_MED_HIGH_AUTO and ib.data.value <= clusters.FanControl.attributes.FanModeSequence.OFF_ON_AUTO then - supported_fan_modes = { "auto", "on" } + supportedFanModes = { "auto", "on" } else - supported_fan_modes = { "on" } + supportedFanModes = { "on" } end else supported_fan_modes_attribute = capabilities.fanMode.supportedFanModes end - -- remove 'off' as a supported fan mode for thermostat device types - if thermostat_utils.get_device_type(device) == fields.THERMOSTAT_DEVICE_TYPE_ID and - device:supports_capability_by_id(capabilities.fanMode.ID) then - -- per the definitions set above, the first index always contains "off" - table.remove(supported_fan_modes, 1) - end - - device:emit_event_for_endpoint(ib.endpoint_id, supported_fan_modes_attribute(supported_fan_modes, {visibility = {displayed = false}})) + local event = supported_fan_modes_attribute(supportedFanModes, {visibility = {displayed = false}}) + device:emit_event_for_endpoint(ib.endpoint_id, event) end function AttributeHandlers.percent_current_handler(driver, device, ib, response) From 91068085afbc5510925d0ac7d3fc1c73f02730e0 Mon Sep 17 00:00:00 2001 From: NoahCornell Date: Tue, 20 Jan 2026 16:52:42 -0600 Subject: [PATCH 383/449] CHAD-17218: Fix Sonos augmented driver store upserts This was broken in hubcore 59 due to IPC rework. --- .../SmartThings/sonos/src/sonos_driver.lua | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/sonos/src/sonos_driver.lua b/drivers/SmartThings/sonos/src/sonos_driver.lua index 1f63ef21d3..56ccb2335f 100644 --- a/drivers/SmartThings/sonos/src/sonos_driver.lua +++ b/drivers/SmartThings/sonos/src/sonos_driver.lua @@ -1,4 +1,6 @@ -local api_version = require "version".api +local version = require "version" +local api_version = version.api +local rpc_version = version.rpc local capabilities = require "st.capabilities" local cosock = require "cosock" local json = require "st.json" @@ -651,6 +653,59 @@ local function do_refresh(driver, device, cmd) sonos_conn:refresh_subscriptions() end +--- In RPC version 100, the events for augmented driver store were changed +--- to no longer match the parsing done in API version 18 and below. +--- +--- API version 19 will handle both before and after RPC 100 changes so this only needs +--- to be applied for RPC version >= 100 and API version <= 18. +if rpc_version >= 100 and api_version <= 18 then + log.info_with({ hub_logs = true }, "Overriding environment info handler for RPC >= 100 and API <= 18") + function SonosDriver:environment_info_handler(channel) + log.info_with({ hub_logs = true }, "Starting environment info handler for RPC >= 100 and API <= 18") + local msg_type, msg_val = channel:receive() + -- This driver only cares about augmentDriverStore messages currently. + -- Previously, this was augmentDatastore msg_type. + if msg_type == "augmentDriverStore" then + if type(msg_val.payload) ~= "table" then + log.warn( + string.format( + "Unexpected augmentDriverStore payload type: %s", + type(msg_val.payload) + ) + ) + return + end + -- The field evt_kind was renamed to kind and is a string of the enum rather than + -- the integer value of the enum. + if msg_val.kind == "Upsert" then + -- This type was changed to table of u8s instead of a string + local data = "" + for _, v in pairs(msg_val.payload.data_value) do + data = data .. string.char(v) + end + self.hub_augmented_driver_data[msg_val.payload.data_key] = data + -- notify with the updated record + if self.notify_augmented_data_changed ~= nil then + self:notify_augmented_data_changed("upsert", msg_val.payload.data_key, data) + end + elseif msg_val.kind == "Delete" then + self.hub_augmented_driver_data[msg_val.payload.data_key] = nil + -- notify with just the key that got deleted + if self.notify_augmented_data_changed ~= nil then + self:notify_augmented_data_changed("delete", msg_val.payload.data_key) + end + else + log.warn( + string.format( + "Unexpected augmentDriverStore kind: %s", + msg_val.kind + ) + ) + end + end + end +end + function SonosDriver.new_driver_template() local oauth_token_bus = cosock.bus() local oauth_info_bus = cosock.bus() @@ -667,6 +722,8 @@ function SonosDriver.new_driver_template() bonded_devices = utils.new_mac_address_keyed_table(), dni_to_device_id = utils.new_mac_address_keyed_table(), lifecycle_handlers = SonosDriverLifecycleHandlers, + -- Only overriden in the case of RPC version 100+ with API version <= 18 + environment_info_handler = SonosDriver.environment_info_handler, capability_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, From 6d0237dba0b1d6148e5094f11cd0dd8144350f61 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 21 Jan 2026 16:00:32 -0800 Subject: [PATCH 384/449] WWSTCERT-9895 WWSTCERT-9936 OREiN Matter smart Bathroom Fan (#2705) * WWSTCERT-9895 OREiN Matter smart Bathroom Fan * WWSTCERT-9936 OREiN Matter smart Bathroom Fan * profile updated to modular --- 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 32a4df6694..a61bc0f0b4 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -161,6 +161,16 @@ matterManufacturer: vendorId: 0x1396 productId: 0x1076 deviceProfileName: light-color-level + - id: "5014/4246" + deviceLabel: OREiN Matter smart Bathroom Fan + vendorId: 0x1396 + productId: 0x1096 + deviceProfileName: light-color-level-fan + - id: "5014/4247" + deviceLabel: OREiN Matter smart Bathroom Fan + vendorId: 0x1396 + productId: 0x1097 + deviceProfileName: fan-modular #Bosch Smart Home - id: "4617/12310" deviceLabel: Plug Compact [M] From c4a69144c153c68a4148146bdc23762f6a3ff947 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 21 Jan 2026 15:56:24 -0800 Subject: [PATCH 385/449] WWSTCERT-9989 Linkind Smart Light Bulb --- 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 a61bc0f0b4..f4b242a44f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -171,6 +171,11 @@ matterManufacturer: vendorId: 0x1396 productId: 0x1097 deviceProfileName: fan-modular + - id: "5014/4215" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1077 + deviceProfileName: light-color-level #Bosch Smart Home - id: "4617/12310" deviceLabel: Plug Compact [M] From c8f0de05162046491ba4625e6383046979e331d1 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Fri, 23 Jan 2026 16:08:22 -0600 Subject: [PATCH 386/449] Add vendor override for Ledvance --- drivers/SmartThings/matter-switch/src/switch_utils/fields.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index f670b154c8..2463201552 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -114,6 +114,8 @@ SwitchFields.switch_category_vendor_overrides = { {0x007D, 0x0074, 0x0075}, [0x1372] = -- Innovation Matters {0x0002}, + [0x1189] = -- Ledvance + {0x0891, 0x0892}, [0x1021] = -- Legrand {0x0005}, [0x109B] = -- Leviton From d908a4353cdcad197e9a0e5dc32a2c2adf4b42e1 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:14:12 -0600 Subject: [PATCH 387/449] Matter Thermostat: Set supportedThermostatOperatingStates if applicable (#2723) --- .../matter-thermostat/src/init.lua | 9 ++++---- .../src/test/test_matter_room_ac.lua | 11 +++++++++- .../src/test/test_matter_room_ac_modular.lua | 3 +++ .../src/test/test_matter_thermo_battery.lua | 4 ++++ .../test/test_matter_thermo_featuremap.lua | 9 +++----- ...st_matter_thermo_multiple_device_types.lua | 6 +++++ .../test_matter_thermo_setpoint_limits.lua | 3 +++ ...test_matter_thermo_setpoint_limits_rpc.lua | 7 ++++++ .../src/test/test_matter_thermostat.lua | 16 ++++++++++++-- ...est_matter_thermostat_composed_bridged.lua | 6 +++++ .../test/test_matter_thermostat_modular.lua | 4 ++++ .../src/test/test_matter_thermostat_rpc5.lua | 7 ++++++ .../src/thermostat_utils/utils.lua | 22 ++++++++++++++++++- 13 files changed, 92 insertions(+), 15 deletions(-) diff --git a/drivers/SmartThings/matter-thermostat/src/init.lua b/drivers/SmartThings/matter-thermostat/src/init.lua index 6697c80fb6..54cb3318f6 100644 --- a/drivers/SmartThings/matter-thermostat/src/init.lua +++ b/drivers/SmartThings/matter-thermostat/src/init.lua @@ -90,6 +90,7 @@ function ThermostatLifecycleHandlers.device_init(driver, device) device:subscribe() device:set_component_to_endpoint_fn(thermostat_utils.component_to_endpoint) device:set_endpoint_to_component_fn(thermostat_utils.endpoint_to_component) + thermostat_utils.handle_thermostat_operating_state_info(device) if not device:get_field(fields.setpoint_limit_device_field.MIN_SETPOINT_DEADBAND_CHECKED) then local auto_eps = device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.AUTOMODE}) --Query min setpoint deadband if needed @@ -100,14 +101,11 @@ function ThermostatLifecycleHandlers.device_init(driver, device) -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. - local electrical_energy_measurement_eps = embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID) - if #electrical_energy_measurement_eps > 0 then - local cumulative_energy_eps = embedded_cluster_utils.get_endpoints( + if #embedded_cluster_utils.get_endpoints( device, clusters.ElectricalEnergyMeasurement.ID, {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY} - ) - if #cumulative_energy_eps == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end + ) == 0 then device:set_field(fields.CUMULATIVE_REPORTS_NOT_SUPPORTED, true, {persist = false}) end end @@ -119,6 +117,7 @@ function ThermostatLifecycleHandlers.info_changed(driver, device, event, args) end if device.profile.id ~= args.old_st_store.profile.id then + thermostat_utils.handle_thermostat_operating_state_info(device) device:subscribe() end end 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 35728e628d..633fb3f1c1 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 @@ -21,7 +21,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode } }, { @@ -32,6 +32,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.Thermostat.ID, cluster_type = "SERVER", feature_map = 0}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0072, device_type_revision = 1 } -- Room Air Conditioner } } } @@ -153,6 +156,9 @@ local function test_init() end end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end @@ -211,6 +217,9 @@ local function test_init_configure() end end end + test.socket.capability:__expect_send( + mock_device_configure:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device_configure.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() 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 96577d72a3..be240c95d0 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 @@ -87,6 +87,9 @@ local function initialize_mock_device(generic_mock_device, generic_subscribed_at end end end + test.socket.capability:__expect_send( + generic_mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) return subscribe_request end 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 9fb8536c23..a28882ca46 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 @@ -4,6 +4,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" local uint32 = require "st.matter.data_types.Uint32" test.set_rpc_version(7) @@ -69,6 +70,9 @@ local function test_init() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "cooling"}, {visibility = {displayed = false}})) + ) test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) 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 c898fab362..2db6e20e59 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 @@ -9,7 +9,7 @@ local clusters = require "st.matter.clusters" test.set_rpc_version(7) local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat-humidity-fan.yml"), + profile = t_utils.get_profile_definition("thermostat-humidity-fan-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -46,7 +46,7 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_simple = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat.yml"), + profile = t_utils.get_profile_definition("thermostat-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -81,7 +81,7 @@ local mock_device_simple = test.mock_device.build_test_matter_device({ }) local mock_device_no_battery = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("thermostat.yml"), + profile = t_utils.get_profile_definition("thermostat-nostate.yml"), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -123,7 +123,6 @@ local cluster_subscribe_list = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -142,7 +141,6 @@ local cluster_subscribe_list_simple = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -158,7 +156,6 @@ local cluster_subscribe_list_no_battery = { clusters.Thermostat.attributes.AbsMinHeatSetpointLimit, clusters.Thermostat.attributes.AbsMaxHeatSetpointLimit, clusters.Thermostat.attributes.SystemMode, - clusters.Thermostat.attributes.ThermostatRunningState, clusters.Thermostat.attributes.ControlSequenceOfOperation, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, 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 2660f75b6e..90e529beb3 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 @@ -149,6 +149,9 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, read_req}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, get_subscribe_request(mock_device, cluster_subscribe_list)}) end test.set_test_init_function(test_init) @@ -167,6 +170,9 @@ local function test_init_disorder_endpoints() test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, read_req}) test.socket.device_lifecycle:__queue_receive({ mock_device_disorder_endpoints.id, "init" }) + test.socket.capability:__expect_send( + mock_device_disorder_endpoints:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device_disorder_endpoints.id, get_subscribe_request( mock_device_disorder_endpoints, cluster_subscribe_list)}) end 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 ae9c25940f..74e550b344 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 @@ -65,6 +65,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() 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 bbd084cb72..13eaa47bfe 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 @@ -4,6 +4,7 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local mock_device = test.mock_device.build_test_matter_device({ @@ -33,6 +34,9 @@ local mock_device = test.mock_device.build_test_matter_device({ }, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY}, {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "BOTH"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -62,6 +66,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) local read_setpoint_deadband = clusters.Thermostat.attributes.MinSetpointDeadBand:read() 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 e40572ff5d..3eed9709a2 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -20,7 +20,7 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.Basic.ID, cluster_type = "SERVER"}, }, device_types = { - device_type_id = 0x0016, device_type_revision = 1, -- RootNode + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode } }, { @@ -36,6 +36,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -70,6 +73,9 @@ local mock_device_auto = test.mock_device.build_test_matter_device({ {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -103,7 +109,10 @@ local function test_init() end end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) + test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) @@ -135,6 +144,9 @@ local function test_init_auto() end end test.socket.matter:__expect_send({mock_device_auto.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device_auto:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device_auto.id, clusters.Thermostat.attributes.MinSetpointDeadBand:read(mock_device_auto)}) test.mock_device.add_test_device(mock_device_auto) end 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 1f1a4e6cb4..ef85b147af 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 @@ -35,6 +35,9 @@ local mock_device = test.mock_device.build_test_matter_device({ { cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER" }, { cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER" }, { cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -67,6 +70,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({ mock_device.id, subscribe_request }) test.mock_device.add_test_device(mock_device) end 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 73d180da7e..bf985671cf 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 @@ -2,6 +2,7 @@ -- 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 im = require "st.matter.interaction_model" @@ -95,6 +96,9 @@ local function test_init() test.socket.matter:__expect_send({ mock_device_basic.id, read_request }) test.socket.device_lifecycle:__queue_receive({ mock_device_basic.id, "init" }) + test.socket.capability:__expect_send( + mock_device_basic:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) subscribe_request_basic = initialize_mock_device(mock_device_basic, subscribed_attributes) end 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 03e7df8ae2..a9bbf59930 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 @@ -2,6 +2,7 @@ -- Licensed under the Apache License, Version 2.0 local clusters = require "st.matter.clusters" +local capabilities = require "st.capabilities" local test = require "integration_test" local t_utils = require "integration_test.utils" local utils = require "st.utils" @@ -39,6 +40,9 @@ local mock_device = test.mock_device.build_test_matter_device({ {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "SERVER"}, {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER"}, + }, + device_types = { + { device_type_id = 0x0301, device_type_revision = 1 } -- Thermostat } } } @@ -70,6 +74,9 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatOperatingState.supportedThermostatOperatingStates({"idle", "heating", "cooling"}, {visibility = {displayed = false}})) + ) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua index 8194f08c48..7773c9ba02 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua @@ -3,8 +3,9 @@ local log = require "log" local capabilities = require "st.capabilities" -local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" +local clusters = require "st.matter.clusters" local fields = require "thermostat_utils.fields" +local embedded_cluster_utils = require "thermostat_utils.embedded_cluster_utils" local ThermostatUtils = {} @@ -85,6 +86,25 @@ function ThermostatUtils.get_endpoints_by_device_type(device, device_type) return endpoints end + -- set the supportedThermostatOperatingStates attribute if the thermostatOperatingState capability is supported and it has not been set before +function ThermostatUtils.handle_thermostat_operating_state_info(device) + local thermostat_operating_state_supported = device:supports_capability(capabilities.thermostatOperatingState) + local latest_supported_operating_states = thermostat_operating_state_supported and device:get_latest_state( + "main", capabilities.thermostatOperatingState.ID, capabilities.thermostatOperatingState.supportedThermostatOperatingStates.NAME + ) + if thermostat_operating_state_supported and latest_supported_operating_states == nil then + local supported_operating_modes = { "idle" } + if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.HEATING}) > 0 then + table.insert(supported_operating_modes, "heating") + end + if #device:get_endpoints(clusters.Thermostat.ID, {feature_bitmap = clusters.Thermostat.types.ThermostatFeature.COOLING}) > 0 then + table.insert(supported_operating_modes, "cooling") + end + local thermostat_ep_id = device:get_endpoints(clusters.Thermostat.ID)[1] + device:emit_event_for_endpoint(thermostat_ep_id, capabilities.thermostatOperatingState.supportedThermostatOperatingStates(supported_operating_modes, {visibility = {displayed = false}})) + end +end + function ThermostatUtils.get_device_type(device) -- For cases where a device has multiple device types, this list indicates which -- device type will be the "main" device type for purposes of selecting a profile From 1646987d4a9d12ceebf516575940a0e2a138f4fc Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Mon, 26 Jan 2026 10:18:09 -0600 Subject: [PATCH 388/449] allow override for non-plugs --- .../src/switch_utils/device_configuration.lua | 8 ++++---- .../src/test/test_multi_switch_parent_child_lights.lua | 2 +- .../src/test/test_multi_switch_parent_child_plugs.lua | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) 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 7ee4b6bf64..ffea9efa2e 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -220,10 +220,10 @@ function DeviceConfiguration.match_profile(driver, device) local generic_profile = function(s) return string.find(updated_profile or "", s, 1, true) end if generic_profile("light-level") and #device:get_endpoints(clusters.OccupancySensing.ID) > 0 then updated_profile = "light-level-motion" - elseif generic_profile("plug-binary") or generic_profile("plug-level") then - if switch_utils.check_switch_category_vendor_overrides(device) then - updated_profile = string.gsub(updated_profile, "plug", "switch") - end + elseif switch_utils.check_switch_category_vendor_overrides(device) then + -- check whether the overwrite should be over "plug" or "light" based on the current profile + local overwrite_category = string.find(updated_profile, "plug") and "plug" or "light" + updated_profile = string.gsub(updated_profile, overwrite_category, "switch") elseif generic_profile("light-level-colorTemperature") or generic_profile("light-color-level") then -- ignore attempts to dynamically profile light-level-colorTemperature and light-color-level devices for now, since -- these may lose fingerprinted Kelvin ranges when dynamically profiled. diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index c0ed244d51..3261360910 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -265,7 +265,7 @@ local function test_init_parent_child_endpoints_non_sequential() test.socket.matter:__expect_send({unsup_mock_device.id, clusters.LevelControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({unsup_mock_device.id, clusters.ColorControl.attributes.Options:write(unsup_mock_device, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - unsup_mock_device:expect_metadata_update({ profile = "light-binary" }) + unsup_mock_device:expect_metadata_update({ profile = "switch-binary" }) unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 09fa39c4f6..92df754303 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -258,7 +258,7 @@ local function test_init_parent_child_endpoints_non_sequential() test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child1_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.LevelControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.LevelControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) test.socket.matter:__expect_send({mock_device_parent_child_endpoints_non_sequential.id, clusters.ColorControl.attributes.Options:write(mock_device_parent_child_endpoints_non_sequential, child2_ep_non_sequential, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) - mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "light-binary" }) + mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ profile = "switch-binary" }) mock_device_parent_child_endpoints_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" }) for _, child in pairs(mock_children_non_sequential) do From 2d6c2951bc6c68776a6431e0743530800cdf9343 Mon Sep 17 00:00:00 2001 From: Konrad Klimczuk Date: Mon, 26 Jan 2026 17:59:56 +0100 Subject: [PATCH 389/449] changes device label according to certification request WWSTCERT-9470 --- drivers/SmartThings/zigbee-button/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index c70cae5eac..c73109ebca 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -220,7 +220,7 @@ zigbeeManufacturer: model: WB01 deviceProfileName: one-button-battery - id: eWeLink/SNZB-01P - deviceLabel: eWeLink Button + deviceLabel: Zigbee Wireless Switch manufacturer: eWeLink model: SNZB-01P deviceProfileName: one-button-battery From 57fe19489fb2fd00faad6a7834c4aad8f4cf8ea6 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:42 -0600 Subject: [PATCH 390/449] CHAD-17046: zigbee-carbon-monoxide-detector subdriver lazy loading --- .../src/ClimaxTechnology/can_handle.lua | 15 +++++++++ .../src/ClimaxTechnology/fingerprints.lua | 9 ++++++ .../src/ClimaxTechnology/init.lua | 31 +++---------------- .../src/init.lua | 18 +++-------- .../src/lazy_load_subdriver.lua | 15 +++++++++ .../src/sub_drivers.lua | 10 ++++++ ...test_climax_technology_carbon_monoxide.lua | 15 ++------- .../src/test/test_zigbee_carbon_monoxide.lua | 15 ++------- 8 files changed, 61 insertions(+), 67 deletions(-) create mode 100644 drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua new file mode 100644 index 0000000000..95ad1a88fa --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_climax_technology_carbon_monoxide = function(opts, driver, device) + local FINGERPRINTS = require("ClimaxTechnology.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ClimaxTechnology") + end + end + + return false +end + +return is_climax_technology_carbon_monoxide diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua new file mode 100644 index 0000000000..d72389f772 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS = { + { mfr = "ClimaxTechnology", model = "CO_00.00.00.22TC" }, + { mfr = "ClimaxTechnology", model = "CO_00.00.00.15TC" } +} + +return CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua index cf96fb7162..1f18983c4b 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/ClimaxTechnology/init.lua @@ -1,33 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" -local CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS = { - { mfr = "ClimaxTechnology", model = "CO_00.00.00.22TC" }, - { mfr = "ClimaxTechnology", model = "CO_00.00.00.15TC" } -} +local capabilities = require "st.capabilities" -local is_climax_technology_carbon_monoxide = function(opts, driver, device) - for _, fingerprint in ipairs(CLIMAX_TECHNOLOGY_CARBON_MONOXIDE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local device_added = function(self, device) device:emit_event(capabilities.battery.battery(100)) @@ -38,7 +15,7 @@ local climax_technology_carbon_monoxide = { lifecycle_handlers = { added = device_added }, - can_handle = is_climax_technology_carbon_monoxide + can_handle = require("ClimaxTechnology.can_handle"), } return climax_technology_carbon_monoxide diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua index 21c170151e..8ef8a50795 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local capabilities = require "st.capabilities" @@ -24,8 +14,8 @@ local zigbee_carbon_monoxide_driver_template = { capabilities.battery, }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, - sub_drivers = { require("ClimaxTechnology") }, health_check = false, + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(zigbee_carbon_monoxide_driver_template, diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua new file mode 100644 index 0000000000..6a7a185392 --- /dev/null +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("ClimaxTechnology") +} + +return sub_drivers 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 a2ba57dfed..78164924d0 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 313f3ea9ec..07933958de 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From 32f481e40969b011d872b996be3942f32b6747db Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:20 -0600 Subject: [PATCH 391/449] CHAD-17066: zigbee-dimmer-remote lazy loading subdrivers --- .../zigbee-dimmer-remote/src/init.lua | 32 ++++++----------- .../src/lazy_load_subdriver.lua | 15 ++++++++ .../zigbee-dimmer-remote/src/sub_drivers.lua | 11 ++++++ .../src/test/test_zigbee_accessory_dimmer.lua | 2 +- .../zigbee-accessory-dimmer/can_handle.lua | 14 ++++++++ .../zigbee-accessory-dimmer/fingerprints.lua | 9 +++++ .../src/zigbee-accessory-dimmer/init.lua | 30 +++------------- .../CentraliteSystems/can_handle.lua | 15 ++++++++ .../CentraliteSystems/fingerprints.lua | 8 +++++ .../CentraliteSystems/init.lua | 30 +++------------- .../IKEAofSweden/can_handle.lua | 15 ++++++++ .../IKEAofSweden/fingerprints.lua | 8 +++++ .../IKEAofSweden/init.lua | 30 +++------------- .../can_handle.lua | 15 ++++++++ .../fingerprints.lua | 10 ++++++ .../zigbee-battery-accessory-dimmer/init.lua | 34 +++---------------- .../sengled/can_handle.lua | 15 ++++++++ .../sengled/fingerprints.lua | 8 +++++ .../sengled/init.lua | 30 +++------------- .../sub_drivers.lua | 12 +++++++ 20 files changed, 188 insertions(+), 155 deletions(-) create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua index 9e45b9dbad..be7f8ad147 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -18,12 +8,12 @@ local defaults = require "st.zigbee.defaults" local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_attribute_configuration = { - cluster = zcl_clusters.PowerConfiguration.ID, - attribute = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID, - minimum_interval = 30, - maximum_interval = 14300, -- ~4hrs - data_type = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.base_type, - reportable_change = 1 + cluster = zcl_clusters.PowerConfiguration.ID, + attribute = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.ID, + minimum_interval = 30, + maximum_interval = 14300, -- ~4hrs + data_type = zcl_clusters.PowerConfiguration.attributes.BatteryPercentageRemaining.base_type, + reportable_change = 1 } local function device_init(driver, device) @@ -38,9 +28,9 @@ local zigbee_dimmer_remote_driver_template = { capabilities.switchLevel }, lifecycle_handlers = { - init = device_init, + init = device_init, }, - sub_drivers = { require("zigbee-accessory-dimmer"), require("zigbee-battery-accessory-dimmer")}, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/sub_drivers.lua new file mode 100644 index 0000000000..c489a2b5d2 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/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("zigbee-accessory-dimmer"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer"), +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua index 477b2eac71..90d29656d8 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..6e87ef0c4b --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_accessory_dimmer = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-accessory-dimmer.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-accessory-dimmer") + end + end + return false +end + +return is_zigbee_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua new file mode 100644 index 0000000000..6528171f35 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS = { + { mfr = "Aurora", model = "Remote50AU" }, + { mfr = "LDS", model = "ZBT-DIMController-D0800" } +} + +return ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua index 91662454c9..b37298802a 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-accessory-dimmer/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -29,10 +19,6 @@ local CURRENT_LEVEL = "current_level" local CURRENT_STATUS = "current_status" local STEP = 10 -local ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS = { - { mfr = "Aurora", model = "Remote50AU" }, - { mfr = "LDS", model = "ZBT-DIMController-D0800" } -} local generate_switch_onoff_event = function(device, value) if value == "on" then @@ -142,14 +128,6 @@ local do_configure = function(self, device) end -local is_zigbee_accessory_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local zigbee_accessory_dimmer = { NAME = "zigbee accessory dimmer", @@ -182,7 +160,7 @@ local zigbee_accessory_dimmer = { added = device_added, doConfigure = do_configure }, - can_handle = is_zigbee_accessory_dimmer + can_handle = require("zigbee-accessory-dimmer.can_handle"), } return zigbee_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua new file mode 100644 index 0000000000..c3dd2870d4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_centralite_systems = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.CentraliteSystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.CentraliteSystems") + end + end + + return false +end + +return is_centralite_systems diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua new file mode 100644 index 0000000000..a8d90850b9 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_SYSTEMS_FINGERPRINTS = { + { mfr = "Centralite Systems", model = "3131-G" } +} + +return CENTRALITE_SYSTEMS_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua index 77bc9fc5a6..a7df73aec5 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/CentraliteSystems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -24,9 +14,6 @@ local capabilities = require "st.capabilities" local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local CENTRALITE_SYSTEMS_FINGERPRINTS = { - { mfr = "Centralite Systems", model = "3131-G" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -77,15 +64,6 @@ local do_configure = function(self, device) device:send(device_management.build_bind_request(device, Level.ID, self.environment_info.hub_zigbee_eui)) end -local is_centralite_systems = function(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_SYSTEMS_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local voltage_configuration = { cluster = zcl_clusters.PowerConfiguration.ID, @@ -118,7 +96,7 @@ local centralite_systems = { init = device_init, doConfigure = do_configure }, - can_handle = is_centralite_systems + can_handle = require("zigbee-battery-accessory-dimmer.CentraliteSystems.can_handle"), } return centralite_systems diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua new file mode 100644 index 0000000000..888e889e6d --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ikea_of_sweden = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.IKEAofSweden.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.IKEAofSweden") + end + end + + return false +end + +return is_ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua new file mode 100644 index 0000000000..429684c8c7 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_OF_SWEDEN_FINGERPRINTS = { + { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" } +} + +return IKEA_OF_SWEDEN_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua index 8edf50b296..4c87146ea2 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/IKEAofSweden/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local utils = require 'st.utils' local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -23,9 +13,6 @@ local capabilities = require "st.capabilities" local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local IKEA_OF_SWEDEN_FINGERPRINTS = { - { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -99,15 +86,6 @@ local battery_perc_attr_handler = function(driver, device, value, zb_rx) device:emit_event(capabilities.battery.battery(utils.clamp_value(value.value, 0, 100))) end -local is_ikea_of_sweden = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_OF_SWEDEN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local ikea_of_sweden = { NAME = "IKEA of Sweden", @@ -126,7 +104,7 @@ local ikea_of_sweden = { } } }, - can_handle = is_ikea_of_sweden + can_handle = require("zigbee-battery-accessory-dimmer.IKEAofSweden.can_handle"), } return ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua new file mode 100644 index 0000000000..7b8f11b278 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer") + end + end + + return false +end + +return is_zigbee_battery_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua new file mode 100644 index 0000000000..16c4a7f7b4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS = { + { mfr = "sengled", model = "E1E-G7F" }, + { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" }, + { mfr = "Centralite Systems", model = "3131-G" } +} + +return ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua index 134931c2c8..01417c1a64 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local zcl_clusters = require "st.zigbee.zcl.clusters" local OnOff = zcl_clusters.OnOff @@ -22,11 +13,6 @@ local SwitchLevel = capabilities.switchLevel local DEFAULT_LEVEL = 100 local DOUBLE_STEP = 10 -local ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS = { - { mfr = "sengled", model = "E1E-G7F" }, - { mfr = "IKEA of Sweden", model = "TRADFRI wireless dimmer" }, - { mfr = "Centralite Systems", model = "3131-G" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -89,15 +75,6 @@ local device_added = function(self, device) end end -local is_zigbee_battery_accessory_dimmer = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_BATTERY_ACCESSORY_DIMMER_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local zigbee_battery_accessory_dimmer = { NAME = "zigbee battery accessory dimmer", @@ -121,8 +98,7 @@ local zigbee_battery_accessory_dimmer = { lifecycle_handlers = { added = device_added }, - sub_drivers = { require("zigbee-battery-accessory-dimmer/CentraliteSystems"), require("zigbee-battery-accessory-dimmer/IKEAofSweden"), require("zigbee-battery-accessory-dimmer/sengled") }, - can_handle = is_zigbee_battery_accessory_dimmer + sub_drivers = require("zigbee-battery-accessory-dimmer.sub_drivers"), } return zigbee_battery_accessory_dimmer diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua new file mode 100644 index 0000000000..5fa64577ca --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-battery-accessory-dimmer.sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-battery-accessory-dimmer.sengled") + end + end + + return false +end + +return is_sengled diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua new file mode 100644 index 0000000000..252c48b6d4 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SENGLED_FINGERPRINTS = { + { mfr = "sengled", model = "E1E-G7F" } +} + +return SENGLED_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua index 08d3dfc4a8..7877c41b68 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sengled/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" @@ -20,9 +10,6 @@ local DOUBLE_STEP = 10 local SENGLED_MFR_SPECIFIC_CLUSTER = 0xFC10 local SENGLED_MFR_SPECIFIC_COMMAND = 0x00 -local SENGLED_FINGERPRINTS = { - { mfr = "sengled", model = "E1E-G7F" } -} local generate_switch_level_event = function(device, value) device:emit_event(capabilities.switchLevel.level(value)) @@ -84,15 +71,6 @@ local sengled_mfr_specific_command_handler = function(driver, device, zb_rx) end end -local is_sengled = function(opts, driver, device) - for _, fingerprint in ipairs(SENGLED_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - - return false -end local sengled = { NAME = "sengled", @@ -103,7 +81,7 @@ local sengled = { } } }, - can_handle = is_sengled + can_handle = require("zigbee-battery-accessory-dimmer.sengled.can_handle"), } return sengled diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua new file mode 100644 index 0000000000..0669f18b25 --- /dev/null +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/zigbee-battery-accessory-dimmer/sub_drivers.lua @@ -0,0 +1,12 @@ +-- 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("zigbee-battery-accessory-dimmer.CentraliteSystems"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer.IKEAofSweden"), + lazy_load_if_possible("zigbee-battery-accessory-dimmer.sengled"), +} + +return sub_drivers From 3179673679e400e1528823c00024f70991c2b3ab Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 26 Jan 2026 09:49:46 -0800 Subject: [PATCH 392/449] Revert "Merge pull request #2662 from haedo-doo/Aqara-DimmerControllerT1-remove-unused-code" This reverts commit 3444bc31f35e684269966915912b42c9065fa78c, reversing changes made to 37f8536890432fa51645ab9dff3aa2be45565744. --- .../SmartThings/zigbee-switch/src/aqara-light/init.lua | 9 +++++++++ .../zigbee-switch/src/test/test_aqara_led_bulb.lua | 3 ++- .../zigbee-switch/src/test/test_aqara_light.lua | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 2192716b21..06d4bf1cc7 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -54,9 +54,18 @@ local function set_level_handler(driver, device, cmd) device:send(Level.commands.MoveToLevelWithOnOff(device, level, dimming_rate)) end +local function init(self, device) + local value = { minimum = 2700, maximum = 6000 } + if device:get_model() == "lumi.light.cwacn1" then + value.maximum = 6500 + end + emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) +end + local aqara_light_handler = { NAME = "Aqara Light Handler", lifecycle_handlers = { + init = init, added = device_added, doConfigure = do_configure }, 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 ce4bb13957..b2f9359850 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 @@ -38,6 +38,7 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -51,7 +52,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + -- test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) 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 c521ba3e44..940131b3e1 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -40,6 +40,7 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -53,7 +54,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + -- test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) From 6ce8ae631da2d15bb16c78ec6f3de6f77ef41304 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Fri, 23 Jan 2026 10:01:21 -0600 Subject: [PATCH 393/449] Fix matter switch refresh capability command --- .../matter-switch/src/switch_utils/fields.lua | 2 ++ .../matter-switch/src/switch_utils/utils.lua | 14 ++++++++++---- .../matter-switch/src/test/test_matter_switch.lua | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 2463201552..ab878698ad 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -83,6 +83,8 @@ SwitchFields.LEVEL_MIN = "__level_min" SwitchFields.LEVEL_MAX = "__level_max" SwitchFields.COLOR_MODE = "__color_mode" +SwitchFields.SUBSCRIBED_ATTRIBUTES_KEY = "__subscribed_attributes" + SwitchFields.updated_fields = { { current_field_name = "__component_to_endpoint_map_button", updated_field_name = SwitchFields.COMPONENT_TO_ENDPOINT_MAP }, { current_field_name = "__switch_intialized", updated_field_name = nil }, diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index c6299fba95..e9809d4e46 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -449,19 +449,21 @@ function utils.populate_subscribe_request_for_device(checked_device, subscribe_r for _, attr in ipairs(subscribed_attributes[capability.id] or {}) do local cluster_id = attr.cluster or attr._cluster.ID local attr_id = attr.ID or attr.attribute - if not attributes_seen[cluster_id..attr_id] then + if not attributes_seen[cluster_id] or not attributes_seen[cluster_id][attr_id] then local ib = im.InteractionInfoBlock(nil, cluster_id, attr_id) subscribe_request:with_info_block(ib) - attributes_seen[cluster_id..attr_id] = true + attributes_seen[cluster_id] = attributes_seen[cluster_id] or {} + attributes_seen[cluster_id][attr_id] = ib end end for _, event in ipairs(subscribed_events[capability.id] or {}) do local cluster_id = event.cluster or event._cluster.ID local event_id = event.ID or event.event - if not events_seen[cluster_id..event_id] then + if not events_seen[cluster_id] or not events_seen[cluster_id][event_id] then local ib = im.InteractionInfoBlock(nil, cluster_id, nil, event_id) subscribe_request:with_info_block(ib) - events_seen[cluster_id..event_id] = true + events_seen[cluster_id] = events_seen[cluster_id] or {} + events_seen[cluster_id][event_id] = ib end end capabilities_seen[capability.id] = true -- only loop through any capability once @@ -486,6 +488,10 @@ 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 #subscribe_request.info_blocks > 0 then device:send(subscribe_request) 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 6a5ecd2cc4..b57c416434 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -1186,4 +1186,19 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Refresh necessary attributes", + function() + test.socket.capability:__queue_receive( + {mock_device.id, {capability = "refresh", component = "main", command = "refresh", args = {}}} + ) + local read_request = cluster_subscribe_list[1]:read(mock_device) + for i, attr in ipairs(cluster_subscribe_list) do + if i > 1 then read_request:merge(attr:read(mock_device)) end + end + test.socket.matter:__expect_send({mock_device.id, read_request}) + test.wait_for_events() + end +) + test.run_registered_tests() From 3dde32dece000274558d78032f7797348d375619 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 26 Jan 2026 12:14:27 -0800 Subject: [PATCH 394/449] fixup Orein fan profile --- drivers/SmartThings/matter-switch/fingerprints.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index f4b242a44f..0871a9c0c5 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -165,9 +165,9 @@ matterManufacturer: deviceLabel: OREiN Matter smart Bathroom Fan vendorId: 0x1396 productId: 0x1096 - deviceProfileName: light-color-level-fan + deviceProfileName: fan-modular - id: "5014/4247" - deviceLabel: OREiN Matter smart Bathroom Fan + deviceLabel: OREiN Matter smart Bathroom Fan vendorId: 0x1396 productId: 0x1097 deviceProfileName: fan-modular From 52ab8d347b684159d8b03339c7c6fe31ac4f9985 Mon Sep 17 00:00:00 2001 From: Jeff Page Date: Tue, 27 Jan 2026 13:38:11 -0600 Subject: [PATCH 395/449] WWSTCERT-9861 Add Zooz ZSE42 to zwave-sensor (for WWST Cert) (#2683) * Add ZSE42 to zwave-sensor * Added sub_driver to handle populating firmewareUpdate.currentVersion * Added ability to set battery quantity and type in configurations * zwave-sensor requested changes * Moved Version:Get from init to added * Added unit test as requested --- .../SmartThings/zwave-sensor/fingerprints.yml | 6 +- .../profiles/zooz-zse42-water.yml | 52 +++++++++++ .../zwave-sensor/src/configurations.lua | 32 +++++++ .../src/firmware-version/init.lua | 91 +++++++++++++++++++ drivers/SmartThings/zwave-sensor/src/init.lua | 1 + .../zwave-sensor/src/preferences.lua | 13 +++ .../src/test/test_firmware_version.lua | 91 +++++++++++++++++++ 7 files changed, 283 insertions(+), 3 deletions(-) create mode 100644 drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml create mode 100644 drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 727f02a2d6..502e5bd5a7 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -502,12 +502,12 @@ zwaveManufacturer: productType: 0x4C47 productId: 0x4C44 deviceProfileName: base-water - - id: 027A/7000/E002 - deviceLabel: Zooz Water Leak Sensor + - id: "Zooz/ZSE42" + deviceLabel: Zooz ZSE42 XS Water Leak Sensor manufacturerId: 0x027A productType: 0x7000 productId: 0xE002 - deviceProfileName: base-water + deviceProfileName: zooz-zse42-water - id: 010F/0800 deviceLabel: Fibaro Motion Sensor manufacturerId: 0x010F diff --git a/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml b/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml new file mode 100644 index 0000000000..1a497e7438 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/profiles/zooz-zse42-water.yml @@ -0,0 +1,52 @@ +name: zooz-zse42-water +components: + - id: main + capabilities: + - id: waterSensor + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: LeakSensor +preferences: + #param 1 + - name: "ledIndicator" + title: "LED Indicator" + description: "When enabled the LED indicator will blink continuously when water is detected." + required: false + preferenceType: enumeration + definition: + options: + 1: "Enabled *" + 0: "Disabled" + default: 1 + #param 2 + - name: "leakAlertClearDelay" + title: "Leak Alert Clear Delay" + description: "Default = 0; How long the sensor will wait before sending a 'dry' report to your hub after water is no longer detected." + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 3600 + default: 0 + #param 4 + - name: "lowBatteryAlert" + title: "Low Battery Alert" + description: "Battery level threshold for low battery reports." + required: false + preferenceType: enumeration + definition: + options: + 10: "10%" + 15: "15%" + 20: "20% *" + 25: "25%" + 30: "30%" + 40: "40%" + 50: "50%" + default: 20 diff --git a/drivers/SmartThings/zwave-sensor/src/configurations.lua b/drivers/SmartThings/zwave-sensor/src/configurations.lua index 89c89428fa..2883e70384 100644 --- a/drivers/SmartThings/zwave-sensor/src/configurations.lua +++ b/drivers/SmartThings/zwave-sensor/src/configurations.lua @@ -12,6 +12,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) --- @type st.zwave.CommandClass.Association @@ -345,6 +346,20 @@ local devices = { ASSOCIATION = { {grouping_identifier = 1} } + }, + ZOOZ_ZSE42_WATER_LEAK = { + MATCHING_MATRIX = { + mfrs = 0x027A, + product_types = 0x7000, + product_ids = 0xE002 + }, + BATTERY = { + quantity = 1, + type = "CR2450" + }, + WAKE_UP = { + { seconds = 21600 } --6 hours + } } } local configurations = {} @@ -376,6 +391,11 @@ configurations.initial_configuration = function(driver, device) device:send(WakeUp:IntervalSet({seconds = value.seconds, node_id = _node_id})) end end + local battery = configurations.get_device_battery(device) + if battery ~= nil then + device:emit_event(capabilities.battery.quantity({ value = battery.quantity or 1 })) + device:emit_event(capabilities.battery.type({ value = battery.type or "Unspecified" })) + end end configurations.get_device_configuration = function(zw_device) @@ -426,4 +446,16 @@ configurations.get_device_wake_up = function(zw_device) return nil end +configurations.get_device_battery = function(zw_device) + for _, device in pairs(devices) do + if zw_device:id_match( + device.MATCHING_MATRIX.mfrs, + device.MATCHING_MATRIX.product_types, + device.MATCHING_MATRIX.product_ids) then + return device.BATTERY + end + end + return nil +end + return configurations diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua new file mode 100644 index 0000000000..058a7f955c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua @@ -0,0 +1,91 @@ +-- Copyright 2025 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" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Version +local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) +--- @type st.zwave.CommandClass.WakeUp +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + +--This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled +local FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor +} + +local function can_handle_fw(opts, driver, device, ...) + if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subDriver = require("firmware-version") + return true, subDriver + end + end + end + return false +end + +--Runs upstream handlers (ex zwave_handlers) +local function call_parent_handler(handlers, self, device, event, args) + for _, func in ipairs(handlers or {}) do + func(self, device, event, args) + end +end + +--Request version if not populated yet +local function send_version_get(driver, device) + if device:get_latest_state("main", capabilities.firmwareUpdate.ID, capabilities.firmwareUpdate.currentVersion.NAME) == nil then + device:send(Version:Get({})) + end +end + +local function version_report(driver, device, cmd) + local major = cmd.args.application_version + local minor = cmd.args.application_sub_version + local fmtFirmwareVersion = string.format("%d.%02d", major, minor) + device:emit_event(capabilities.firmwareUpdate.currentVersion({ value = fmtFirmwareVersion })) +end + +local function wakeup_notification(driver, device, cmd) + send_version_get(driver, device) + --Call parent WakeUp functions + call_parent_handler(driver.zwave_handlers[cc.WAKE_UP][WakeUp.NOTIFICATION], driver, device, cmd) +end + +local function added_handler(driver, device) + --Call main function + driver.lifecycle_handlers.added(driver, device) + --Extras for this sub_driver + send_version_get(driver, device) +end + +local firmware_version = { + NAME = "firmware_version", + can_handle = can_handle_fw, + + lifecycle_handlers = { + added = added_handler, + }, + zwave_handlers = { + [cc.VERSION] = { + [Version.REPORT] = version_report + }, + [cc.WAKE_UP] = { + [WakeUp.NOTIFICATION] = wakeup_notification + } + } +} + +return firmware_version \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index d92a09fe56..213aa8c389 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -152,6 +152,7 @@ local driver_template = { lazy_load_if_possible("v1-contact-event"), lazy_load_if_possible("timed-tamper-clear"), lazy_load_if_possible("wakeup-no-poll"), + lazy_load_if_possible("firmware-version"), lazy_load_if_possible("apiv6_bugfix"), }, lifecycle_handlers = { diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 4921a996bf..9585b6ffe9 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -162,6 +162,19 @@ local devices = { motionNotdetRepT = {parameter_number = 160, size = 2}, }, }, + ZOOZ_ZSE42_WATER_LEAK = { + MATCHING_MATRIX = { + mfrs = 0x027A, + product_types = 0x7000, + product_ids = 0xE002 + }, + PARAMETERS = { + ledIndicator = { parameter_number = 1, size = 1 }, + leakAlertClearDelay = { parameter_number = 2, size = 4 }, + batteryThreshold = { parameter_number = 3, size = 1 }, + lowBatteryAlert = { parameter_number = 4, size = 1 }, + }, + }, } local preferences = {} diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua new file mode 100644 index 0000000000..9790b5cfb5 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua @@ -0,0 +1,91 @@ +-- 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 test = require "integration_test" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" +--- @type st.zwave.CommandClass.Version +local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) +--- @type st.zwave.CommandClass.WakeUp +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) + +local sensor_endpoints = { + { + command_classes = { + { value = cc.WAKE_UP }, + { value = cc.BATTERY }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("zooz-zse42-water.yml"), + zwave_endpoints = sensor_endpoints, + zwave_manufacturer_id = 0x027A, + zwave_product_type = 0x7000, + zwave_product_id = 0xE002, +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "Wakeup notification should not poll binary sensor if device has contact state", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(WakeUp:Notification({ })) } + }, + --This is sent by the sub-driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Version:Get({}) + ) + }, + --This is sent by the main driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + WakeUp:IntervalGetV1({}) + ) + }, + --This is sent by the main driver + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Battery:Get({ }) + ) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() \ No newline at end of file From da4e8ea5f5b00c36e8bfc980dffc9ee0bd1980e2 Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:41:58 +0100 Subject: [PATCH 396/449] WWSTCERT-9438 Add frient io module driver (#2600) * initial commit * test changes * add register_native_switch_handler * tests WIP * working tests * get rid of unused variables * test * additional test * Revert changes and add can_handle file * fixed test * capabilities fix * Changes according to pr comments * changes according to pr comments * Shortened Copyright statement --- .../zigbee-switch/fingerprints.yml | 5 + .../profiles/frient-io-output-switch.yml | 25 + .../profiles/switch-4inputs-2outputs.yml | 107 +++ .../src/configurations/devices.lua | 61 ++ .../src/frient-IO/can_handle.lua | 12 + .../zigbee-switch/src/frient-IO/init.lua | 488 +++++++++++++ .../src/frient-IO/unbind_request.lua | 84 +++ .../SmartThings/zigbee-switch/src/init.lua | 3 +- .../src/test/test_frient_IO_module.lua | 683 ++++++++++++++++++ 9 files changed, 1467 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml create mode 100644 drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 3d598372fa..a83204e5f6 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -505,6 +505,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: SMRZB-342 deviceProfileName: frient-switch-power-energy-voltage + - id: "frient/IOMZB-110" + deviceLabel: frient IO Module + manufacturer: frient A/S + model: IOMZB-110 + deviceProfileName: switch-4inputs-2outputs - id: "AduroSmart Eria/AD-DimmableLight3001" deviceLabel: Eria Light manufacturer: AduroSmart Eria diff --git a/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml b/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml new file mode 100644 index 0000000000..551061d7b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/frient-io-output-switch.yml @@ -0,0 +1,25 @@ +name: frient-io-output-switch +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 +preferences: + - title: "Output: On Time" + name: configOnTime + required: true + preferenceType: integer + definition: + minimum: 0 + maximum: 6553 + default: 0 + - title: "Output: Off Wait Time" + name: configOffWaitTime + required: true + preferenceType: integer + definition: + minimum: 0 + maximum: 6553 + default: 0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml b/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml new file mode 100644 index 0000000000..c6120b2561 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/switch-4inputs-2outputs.yml @@ -0,0 +1,107 @@ +name: switch-4inputs-2outputs +components: + - id: main + capabilities: + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch + - id: input1 + label: "Input 1" + capabilities: + - id: switch + version: 1 + - id: input2 + label: "Input 2" + capabilities: + - id: switch + version: 1 + - id: input3 + label: "Input 3" + capabilities: + - id: switch + version: 1 + - id: input4 + label: "Input 4" + capabilities: + - id: switch + version: 1 +preferences: + # Input 1 + - title: "Input 1: Reverse Polarity" + name: reversePolarity1 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 1: Control Output 1" + name: controlOutput11 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 1: Control Output 2" + name: controlOutput21 + required: true + preferenceType: boolean + definition: + default: false + # Input 2 + - title: "Input 2: Reverse Polarity" + name: reversePolarity2 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 2: Control Output 1" + name: controlOutput12 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 2: Control Output 2" + name: controlOutput22 + required: true + preferenceType: boolean + definition: + default: false + # Input 3 + - title: "Input 3: Reverse Polarity" + name: reversePolarity3 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 3: Control Output 1" + name: controlOutput13 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 3: Control Output 2" + name: controlOutput23 + required: true + preferenceType: boolean + definition: + default: false + # Input 4 + - title: "Input 4: Reverse Polarity" + name: reversePolarity4 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 4: Control Output 1" + name: controlOutput14 + required: true + preferenceType: boolean + definition: + default: false + - title: "Input 4: Control Output 2" + name: controlOutput24 + required: true + preferenceType: boolean + definition: + default: false \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua index 9910dde229..d49d10e9c5 100644 --- a/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua +++ b/drivers/SmartThings/zigbee-switch/src/configurations/devices.lua @@ -7,7 +7,10 @@ local IASZone = clusters.IASZone local ElectricalMeasurement = clusters.ElectricalMeasurement local SimpleMetering = clusters.SimpleMetering local Alarms = clusters.Alarms +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff local constants = require "st.zigbee.constants" +local data_types = require "st.zigbee.data_types" local devices = { IKEA_RGB_BULB = { @@ -110,6 +113,64 @@ local devices = { }, } }, + FRIENT_IO_MODULE = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "IOMZB-110" } + }, + CONFIGURATION = { + { + cluster = OnOff.ID, + attribute = OnOff.attributes.OnTime.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 1, + data_type = OnOff.attributes.OnOff.base_type, + configurable = true, + monitored = true + }, + { + cluster = OnOff.ID, + attribute = OnOff.attributes.OffWaitTime.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 1, + data_type = OnOff.attributes.OffWaitTime.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.PresentValue.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = BasicInput.attributes.PresentValue.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = BasicInput.attributes.Polarity.ID, + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = BasicInput.attributes.Polarity.base_type, + configurable = true, + monitored = true + }, + { + cluster = BasicInput.ID, + attribute = 0x8000, -- IASActivation + minimum_interval = 10, + maximum_interval = 600, + reportable_change = 0, + data_type = data_types.Uint16, + mfg_code = 0x1015, + configurable = true, + monitored = true + } + } + } } return devices \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua new file mode 100644 index 0000000000..4e1d465ee3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Function to determine if the driver can handle this device +return function(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and device:get_model() == "IOMZB-110" then + local subdriver = require("frient-IO") + return true, subdriver + else + return false + end +end diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua new file mode 100644 index 0000000000..293583f442 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua @@ -0,0 +1,488 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Zigbee Spec Utils +local constants = require "st.zigbee.constants" +local messages = require "st.zigbee.messages" +local zdo_messages = require "st.zigbee.zdo" +local bind_request = require "st.zigbee.zdo.bind_request" +local unbind_request = require "frient-IO.unbind_request" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local zcl_global_commands = require "st.zigbee.zcl.global_commands" +local Status = require "st.zigbee.generated.types.ZclStatus" + +local clusters = require "st.zigbee.zcl.clusters" +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff +-- Capabilities +local capabilities = require "st.capabilities" +local Switch = capabilities.switch +local CHILD_OUTPUT_PROFILE = "frient-io-output-switch" +local utils = require "st.utils" + +local configurationMap = require "configurations" + +local COMPONENTS = { + INPUT_1 = "input1", + INPUT_2 = "input2", + INPUT_3 = "input3", + INPUT_4 = "input4", + OUTPUT_1 = "output1", + OUTPUT_2 = "output2" +} + +local ZIGBEE_ENDPOINTS = { + INPUT_1 = 0x70, + INPUT_2 = 0x71, + INPUT_3 = 0x72, + INPUT_4 = 0x73, + OUTPUT_1 = 0x74, + OUTPUT_2 = 0x75 +} + +local INPUT_CONFIGS = { + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_1, + reverse_pref = "reversePolarity1", + binds = { + { pref = "controlOutput11", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput21", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_2, + reverse_pref = "reversePolarity2", + binds = { + { pref = "controlOutput12", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput22", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_3, + reverse_pref = "reversePolarity3", + binds = { + { pref = "controlOutput13", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput23", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_4, + reverse_pref = "reversePolarity4", + binds = { + { pref = "controlOutput14", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1 }, + { pref = "controlOutput24", endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2 } + } + } +} + +local OUTPUT_INFO = { + ["1"] = { endpoint = ZIGBEE_ENDPOINTS.OUTPUT_1, key = "frient-io-output-1", label_suffix = "Output 1" }, + ["2"] = { endpoint = ZIGBEE_ENDPOINTS.OUTPUT_2, key = "frient-io-output-2", label_suffix = "Output 2" } +} + +local OUTPUT_BY_ENDPOINT, OUTPUT_BY_KEY = {}, {} +for suffix, info in pairs(OUTPUT_INFO) do + info.suffix = suffix + OUTPUT_BY_ENDPOINT[info.endpoint] = info + OUTPUT_BY_KEY[info.key] = info +end + +local ZIGBEE_MFG_CODES = { + Develco = 0x1015 +} + +local ZIGBEE_MFG_ATTRIBUTES = { + client = { + OnWithTimeOff_OnTime = { + ID = 0x8000, + data_type = data_types.Uint16 + }, + OnWithTimeOff_OffWaitTime = { + ID = 0x8001, + data_type = data_types.Uint16 + } + }, + server = { IASActivation = { + ID = 0x8000, + data_type = data_types.Uint16 + } } +} + +local function write_client_manufacturer_specific_attribute(device, cluster_id, attr_id, mfg_specific_code, data_type, + payload) + local message = cluster_base.write_manufacturer_specific_attribute(device, cluster_id, attr_id, mfg_specific_code, + data_type, payload) + + message.body.zcl_header.frame_ctrl:set_direction_client() + return message +end + +local function write_basic_input_polarity_attr(device, ep_id, payload) + local value = data_types.validate_or_build_type(payload and 1 or 0, + BasicInput.attributes.Polarity.base_type, + "payload") + device:send(cluster_base.write_attribute(device, data_types.ClusterId(BasicInput.ID), + data_types.AttributeId(BasicInput.attributes.Polarity.ID), + value):to_endpoint(ep_id)) +end + +local function ensure_child_devices(driver, device) + if device.parent_assigned_child_key ~= nil then + return + end + + for _, info in pairs(OUTPUT_INFO) do + local child = device:get_child_by_parent_assigned_key(info.key) + if child == nil then + driver:try_create_device({ + type = "EDGE_CHILD", + parent_device_id = device.id, + parent_assigned_child_key = info.key, + profile = CHILD_OUTPUT_PROFILE, + label = string.format("%s %s", device.label, info.label_suffix), + vendor_provided_label = info.label_suffix + }) + child = device:get_child_by_parent_assigned_key(info.key) + end + if child then + child:set_field("endpoint", info.endpoint, { persist = true }) + end + end +end + +local function sanitize_timing(value) + local int = math.tointeger(value) or 0 + return utils.clamp_value(int, 0, 0xFFFF) +end + +local function get_output_timing(device, suffix) + local info = OUTPUT_INFO[suffix] + if not info then return 0, 0 end + local child = device:get_child_by_parent_assigned_key(info.key) + local on_time = math.floor((sanitize_timing(device.preferences["configOnTime" .. suffix]))*10) + local off_wait = math.floor((sanitize_timing(device.preferences["configOffWaitTime" .. suffix]))*10) + if child then + on_time = math.floor((sanitize_timing(child.preferences.configOnTime)) * 10) + off_wait = math.floor((sanitize_timing(child.preferences.configOffWaitTime)) * 10) + end + return on_time, off_wait +end + +local function handle_output_command(device, suffix, command_name) + local info = OUTPUT_INFO[suffix] + if info == nil then return end + local config_on_time, config_off_wait_time = get_output_timing(device, suffix) + local endpoint = info.endpoint + + if command_name == "on" then + if config_on_time == 0 then + device:send(OnOff.server.commands.On(device):to_endpoint(endpoint)) + else + device:send(OnOff.server.commands.OnWithTimedOff(device, data_types.Uint8(0), + data_types.Uint16(config_on_time), data_types.Uint16(config_off_wait_time)):to_endpoint(endpoint)) + end + else + device:send(OnOff.server.commands.Off(device):to_endpoint(endpoint)) + end +end + +local function emit_switch_event_for_endpoint(device, endpoint, event) + local info = OUTPUT_BY_ENDPOINT[endpoint] + if info ~= nil then + local child = device:get_child_by_parent_assigned_key(info.key) + if child then + child:emit_event(event) + return + end + end + device:emit_event_for_endpoint(endpoint, event) +end + +local function register_native_switch_handler(device, endpoint) + local field_key = string.format("frient_io_native_%02X", endpoint) + local info = OUTPUT_BY_ENDPOINT[endpoint] + if info ~= nil then + local child = device:get_child_by_parent_assigned_key(info.key) + if child and not child:get_field(field_key) then + child:register_native_capability_attr_handler("switch", "switch") + child:set_field(field_key, true) + end + return + end + + if not device:get_field(field_key) then + device:register_native_capability_attr_handler("switch", "switch") + device:set_field(field_key, true) + end +end + +local function on_off_attr_handler(driver, device, value, zb_message) + local endpoint = zb_message.address_header.src_endpoint.value + register_native_switch_handler(device, endpoint) + emit_switch_event_for_endpoint(device, endpoint, value.value and Switch.switch.on() or Switch.switch.off()) +end + +local function build_bind_request(device, src_cluster, src_ep_id, dest_ep_id) + local addr_header = messages.AddressHeader(constants.HUB.ADDR, constants.HUB.ENDPOINT, device:get_short_address(), + device.fingerprinted_endpoint_id, constants.ZDO_PROFILE_ID, bind_request.BindRequest.ID) + + local bind_req = bind_request.BindRequest(device.zigbee_eui, src_ep_id, + src_cluster, + bind_request.ADDRESS_MODE_64_BIT, device.zigbee_eui, dest_ep_id) + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = bind_req + }) + local bind_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + return bind_cmd +end + +local function build_unbind_request(device, src_cluster, src_ep_id, dest_ep_id) + local addr_header = messages.AddressHeader(constants.HUB.ADDR, constants.HUB.ENDPOINT, device:get_short_address(), + device.fingerprinted_endpoint_id, constants.ZDO_PROFILE_ID, unbind_request.UNBIND_REQUEST_CLUSTER_ID) + + local unbind_req = unbind_request.UnbindRequest(device.zigbee_eui, src_ep_id, + src_cluster, + unbind_request.ADDRESS_MODE_64_BIT, device.zigbee_eui, dest_ep_id) + local message_body = zdo_messages.ZdoMessageBody({ + zdo_body = unbind_req + }) + local bind_cmd = messages.ZigbeeMessageTx({ + address_header = addr_header, + body = message_body + }) + return bind_cmd +end + +local function apply_input_preference_changes(device, old_prefs, config) + if old_prefs[config.reverse_pref] ~= device.preferences[config.reverse_pref] then + write_basic_input_polarity_attr(device, config.endpoint, device.preferences[config.reverse_pref]) + end + + for _, bind_cfg in ipairs(config.binds) do + if old_prefs[bind_cfg.pref] ~= device.preferences[bind_cfg.pref] then + device:send(device.preferences[bind_cfg.pref] + and build_bind_request(device, BasicInput.ID, config.endpoint, bind_cfg.endpoint) + or build_unbind_request(device, BasicInput.ID, config.endpoint, bind_cfg.endpoint)) + end + end +end + +local function component_to_endpoint(device, component_id) + if component_id == COMPONENTS.INPUT_1 then + return ZIGBEE_ENDPOINTS.INPUT_1 + elseif component_id == COMPONENTS.INPUT_2 then + return ZIGBEE_ENDPOINTS.INPUT_2 + elseif component_id == COMPONENTS.INPUT_3 then + return ZIGBEE_ENDPOINTS.INPUT_3 + elseif component_id == COMPONENTS.INPUT_4 then + return ZIGBEE_ENDPOINTS.INPUT_4 + elseif component_id == COMPONENTS.OUTPUT_1 then + return ZIGBEE_ENDPOINTS.OUTPUT_1 + elseif component_id == COMPONENTS.OUTPUT_2 then + return ZIGBEE_ENDPOINTS.OUTPUT_2 + else + return device.fingerprinted_endpoint_id + end +end + +local function endpoint_to_component(device, ep) + local ep_id = type(ep) == "table" and ep.value or ep + if ep_id == ZIGBEE_ENDPOINTS.INPUT_1 then + return COMPONENTS.INPUT_1 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_2 then + return COMPONENTS.INPUT_2 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_3 then + return COMPONENTS.INPUT_3 + elseif ep_id == ZIGBEE_ENDPOINTS.INPUT_4 then + return COMPONENTS.INPUT_4 + elseif ep_id == ZIGBEE_ENDPOINTS.OUTPUT_1 then + return COMPONENTS.OUTPUT_1 + elseif ep_id == ZIGBEE_ENDPOINTS.OUTPUT_2 then + return COMPONENTS.OUTPUT_2 + else + return "main" + end +end + +local function init_handler(self, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local function added_handler(self, device) + ensure_child_devices(self, device) +end + +local function configure_handler(self, device) + local configuration = configurationMap.get_device_configuration(device) + if configuration ~= nil then + for _, attribute in ipairs(configuration) do + if attribute.configurable ~= false then + device:add_configured_attribute(attribute) + end + end + end + device:configure() + if device.parent_assigned_child_key ~= nil then + return + end + + ensure_child_devices(self, device) + + local on1, off1 = get_output_timing(device, "1") + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on1):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1)) + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off1):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1)) + + local on2, off2 = get_output_timing(device, "2") + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on2):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2)) + device:send(write_client_manufacturer_specific_attribute(device, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off2):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2)) + + -- Input 1 + local default_old_prefs = {} + for _, config in ipairs(INPUT_CONFIGS) do + apply_input_preference_changes(device, default_old_prefs, config) + end +end + +local function info_changed_handler(self, device, event, args) + if device.parent_assigned_child_key ~= nil then + -- This is a child device + local parent = device:get_parent_device() + if not parent then return end + + local info = OUTPUT_BY_KEY[device.parent_assigned_child_key] + if not info then return end + + -- Child devices have simple preference names without suffix + local on_time = math.floor(sanitize_timing(device.preferences.configOnTime) * 10) + local off_wait = math.floor(sanitize_timing(device.preferences.configOffWaitTime) * 10) + + parent:send(write_client_manufacturer_specific_attribute(parent, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OnTime.data_type, + on_time):to_endpoint(info.endpoint)) + + parent:send(write_client_manufacturer_specific_attribute(parent, BasicInput.ID, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.ID, ZIGBEE_MFG_CODES.Develco, + ZIGBEE_MFG_ATTRIBUTES.client.OnWithTimeOff_OffWaitTime.data_type, + off_wait):to_endpoint(info.endpoint)) + return + else + local old_prefs = (args.old_st_store and args.old_st_store.preferences) or {} + for _, config in ipairs(INPUT_CONFIGS) do + apply_input_preference_changes(device, old_prefs, config) + end + end +end + +local function present_value_attr_handler(driver, device, value, zb_message) + local ep_id = zb_message.address_header.src_endpoint + register_native_switch_handler(device, ep_id.value) + device:emit_event_for_endpoint(ep_id, value.value and Switch.switch.on() or Switch.switch.off()) +end + +local function on_off_default_response_handler(driver, device, zb_rx) + local status = zb_rx.body.zcl_body.status.value + local endpoint = zb_rx.address_header.src_endpoint.value + + if status == Status.SUCCESS then + local cmd = zb_rx.body.zcl_body.cmd.value + local event = nil + + if cmd == OnOff.server.commands.On.ID then + event = Switch.switch.on() + elseif cmd == OnOff.server.commands.OnWithTimedOff.ID then + device:send(cluster_base.read_attribute(device, data_types.ClusterId(OnOff.ID), + data_types.AttributeId(OnOff.attributes.OnOff.ID)):to_endpoint(endpoint)) + elseif cmd == OnOff.server.commands.Off.ID then + event = Switch.switch.off() + end + + if event ~= nil then + emit_switch_event_for_endpoint(device, endpoint, event) + end + end +end + +local function make_switch_handler(command_name) + return function(driver, device, command) + local parent = device:get_parent_device() + if parent then + local info = OUTPUT_BY_KEY[device.parent_assigned_child_key] + if info then + handle_output_command(parent, info.suffix, command_name) + return + end + end + + local num = command.component and command.component:match("output(%d)") + if num then + handle_output_command(device, num, command_name) + return + end + num = command.component:match("input(%d)") + if num then + local component = device.profile.components[command.component] + local value = device:get_latest_state(command.component, Switch.ID, Switch.switch.NAME) + if value == "on" then + device:emit_component_event(component, + Switch.switch.on({ state_change = true, visibility = { displayed = false } })) + elseif value == "off" then + device:emit_component_event(component, + Switch.switch.off({ state_change = true, visibility = { displayed = false } })) + end + end + end +end + +local frient_bridge_handler = { + NAME = "frient bridge handler", + zigbee_handlers = { + global = { + [OnOff.ID] = { + [zcl_global_commands.DEFAULT_RESPONSE_ID] = on_off_default_response_handler + } + }, + cluster = {}, + attr = { + [BasicInput.ID] = { + [BasicInput.attributes.PresentValue.ID] = present_value_attr_handler + }, + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = on_off_attr_handler + } + }, + zdo = {} + }, + capability_handlers = { + [Switch.ID] = { + [Switch.commands.on.NAME] = make_switch_handler("on"), + [Switch.commands.off.NAME] = make_switch_handler("off") + } + }, + lifecycle_handlers = { + added = added_handler, + init = init_handler, + doConfigure = configure_handler, + infoChanged = info_changed_handler + }, + can_handle = require("frient-IO.can_handle"), +} + +return frient_bridge_handler diff --git a/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua b/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua new file mode 100644 index 0000000000..64c4b05464 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/frient-IO/unbind_request.lua @@ -0,0 +1,84 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local data_types = require "st.zigbee.data_types" +local utils = require "st.zigbee.utils" + +local unbind_request = {} + +unbind_request.UNBIND_REQUEST_CLUSTER_ID = 0x0022 +unbind_request.ADDRESS_MODE_16_BIT = 0x01 +unbind_request.ADDRESS_MODE_64_BIT = 0x03 + +local UnbindRequest = { + ID = unbind_request.UNBIND_REQUEST_CLUSTER_ID, + NAME = "UnbindRequest", +} +UnbindRequest.__index = UnbindRequest +unbind_request.UnbindRequest = UnbindRequest + +function UnbindRequest.deserialize(buf) + local self = {} + setmetatable(self, UnbindRequest) + + local fields = { + { name = "src_address", type = data_types.IeeeAddress }, + { name = "src_endpoint", type = data_types.Uint8 }, + { name = "cluster_id", type = data_types.ClusterId }, + { name = "dest_addr_mode", type = data_types.Uint8 }, + } + utils.deserialize_field_list(self, fields, buf) + + if self.dest_addr_mode.value == unbind_request.ADDRESS_MODE_16_BIT then + self.dest_address = data_types.Uint16.deserialize(buf) + else + self.dest_address = data_types.IeeeAddress.deserialize(buf) + self.dest_endpoint = data_types.Uint8.deserialize(buf) + end + return self +end + +--- A helper function used by common code to get all the component pieces of this message frame +function UnbindRequest:get_fields() + local out = {} + out[#out + 1] = self.src_address + out[#out + 1] = self.src_endpoint + out[#out + 1] = self.cluster_id + out[#out + 1] = self.dest_addr_mode + out[#out + 1] = self.dest_address + if self.dest_addr_mode.value == unbind_request.ADDRESS_MODE_64_BIT then + out[#out + 1] = self.dest_endpoint + end + return out +end + +UnbindRequest.get_length = utils.length_from_fields +UnbindRequest._serialize = utils.serialize_from_fields +UnbindRequest.pretty_print = utils.print_from_fields +UnbindRequest.__tostring = UnbindRequest.pretty_print +function UnbindRequest.from_values(orig, src_address, src_endpoint, cluster_id, dest_addr_mode, dest_address, + dest_endpoint) + local out = {} + if src_address == nil or src_endpoint == nil or cluster_id == nil or dest_addr_mode == nil or dest_address == nil then + error("Missing necessary values for bind request", 2) + end + + out.src_address = data_types.validate_or_build_type(src_address, data_types.IeeeAddress, "src_address") + out.src_endpoint = data_types.validate_or_build_type(src_endpoint, data_types.Uint8, "src_endpoint") + out.cluster_id = data_types.validate_or_build_type(cluster_id, data_types.ClusterId, "cluster") + out.dest_addr_mode = data_types.validate_or_build_type(dest_addr_mode, data_types.Uint8, "dest_addr_mode") + if (out.dest_addr_mode.value == unbind_request.ADDRESS_MODE_16_BIT) then + out.dest_address = data_types.validate_or_build_type(dest_address, data_types.Uint16, "dest_address") + elseif out.dest_addr_mode.value == unbind_request.ADDRESS_MODE_64_BIT then + out.dest_address = data_types.validate_or_build_type(dest_address, data_types.IeeeAddress, "dest_address") + out.dest_endpoint = data_types.validate_or_build_type(dest_endpoint, data_types.Uint8, "dest_endpoint") + else + error(string.format("Unrecognized destination address mode: %d", out.dest_addr_mode.value), 2) + end + + setmetatable(out, UnbindRequest) + return out +end + +setmetatable(unbind_request.UnbindRequest, { __call = unbind_request.UnbindRequest.from_values }) + +return unbind_request diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 1b694d2a5e..a7be3f8801 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -98,7 +98,8 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi"), - lazy_load_if_possible("frient") + lazy_load_if_possible("frient"), + lazy_load_if_possible("frient-IO") }, zigbee_handlers = { global = { 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 new file mode 100644 index 0000000000..c9d5f9b75e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua @@ -0,0 +1,683 @@ +-- Copyright 2025 SmartThings +-- Licensed under the Apache License, Version 2.0 + +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 capabilities = require "st.capabilities" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local messages = require "st.zigbee.messages" +local constants = require "st.zigbee.constants" +local zdo_messages = require "st.zigbee.zdo" +local bind_request = require "st.zigbee.zdo.bind_request" +local unbind_request = require "frient-IO.unbind_request" +local default_response = require "st.zigbee.zcl.global_commands.default_response" +local zcl_messages = require "st.zigbee.zcl" +local Status = require "st.zigbee.generated.types.ZclStatus" +local device_management = require "st.zigbee.device_management" +local configuration_map = require "configurations" +local switch_defaults = require "st.zigbee.defaults.switch_defaults" +local mock_devices_api = require "integration_test.mock_devices_api" + +local BasicInput = clusters.BasicInput +local OnOff = clusters.OnOff +local Switch = capabilities.switch + +local ZIGBEE_ENDPOINTS = { + INPUT_1 = 0x70, + INPUT_2 = 0x71, + INPUT_3 = 0x72, + INPUT_4 = 0x73, + OUTPUT_1 = 0x74, + OUTPUT_2 = 0x75, +} + +local INPUT_CONFIGS = { + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_1, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_2, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_3, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, + { + endpoint = ZIGBEE_ENDPOINTS.INPUT_4, + binds = { + ZIGBEE_ENDPOINTS.OUTPUT_1, + ZIGBEE_ENDPOINTS.OUTPUT_2, + }, + }, +} + +local DEVELCO_MFG_CODE = 0x1015 +local ON_TIME_ATTR = 0x8000 +local OFF_WAIT_ATTR = 0x8001 + +local function sanitize_timing(value) + local v = tonumber(value) or 0 + if v < 0 then + v = 0 + elseif v > 0xFFFF then + v = 0xFFFF + end + return math.tointeger(v) or 0 +end + +local function to_deciseconds(value) + return math.floor(sanitize_timing(value) * 10) +end + +local function build_client_mfg_write(device, endpoint, attr_id, value) + local msg = cluster_base.write_manufacturer_specific_attribute( + device, + BasicInput.ID, + attr_id, + DEVELCO_MFG_CODE, + data_types.Uint16, + value + ) + msg.body.zcl_header.frame_ctrl:set_direction_client() + msg.tx_options = data_types.Uint16(0) + return msg:to_endpoint(endpoint) +end + +local function build_basic_input_polarity_write(device, endpoint, enabled) + local polarity_value = data_types.validate_or_build_type( + enabled and 1 or 0, + BasicInput.attributes.Polarity.base_type, + "payload" + ) + local msg = cluster_base.write_attribute( + device, + data_types.ClusterId(BasicInput.ID), + data_types.AttributeId(BasicInput.attributes.Polarity.ID), + polarity_value + ) + msg.tx_options = data_types.Uint16(0) + return msg:to_endpoint(endpoint) +end + +local function build_bind(device, src_ep, dest_ep) + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + bind_request.BindRequest.ID + ) + local bind_body = bind_request.BindRequest( + device.zigbee_eui, + src_ep, + BasicInput.ID, + bind_request.ADDRESS_MODE_64_BIT, + device.zigbee_eui, + dest_ep + ) + local message_body = zdo_messages.ZdoMessageBody({ zdo_body = bind_body }) + local msg = messages.ZigbeeMessageTx({ address_header = addr_header, body = message_body }) + msg.tx_options = data_types.Uint16(0) + return msg +end + +local function build_unbind(device, src_ep, dest_ep) + local addr_header = messages.AddressHeader( + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + device:get_short_address(), + device.fingerprinted_endpoint_id, + constants.ZDO_PROFILE_ID, + unbind_request.UNBIND_REQUEST_CLUSTER_ID + ) + local unbind_body = unbind_request.UnbindRequest( + device.zigbee_eui, + src_ep, + BasicInput.ID, + unbind_request.ADDRESS_MODE_64_BIT, + device.zigbee_eui, + dest_ep + ) + local message_body = zdo_messages.ZdoMessageBody({ zdo_body = unbind_body }) + local msg = messages.ZigbeeMessageTx({ address_header = addr_header, body = message_body }) + msg.tx_options = data_types.Uint16(0) + return msg +end + +local function build_default_response_msg(device, endpoint, command_id) + local addr_header = messages.AddressHeader( + device:get_short_address(), + endpoint, + constants.HUB.ADDR, + constants.HUB.ENDPOINT, + constants.HA_PROFILE_ID, + OnOff.ID + ) + local response_body = default_response.DefaultResponse(command_id, Status.SUCCESS) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = response_body + }) + return messages.ZigbeeMessageRx({ address_header = addr_header, body = message_body }) +end + +local function build_output_timing(device, child, suffix) + local on_pref + local off_pref + if child.preferences.configOnTime ~= nil or child.preferences.configOffWaitTime ~= nil then + on_pref = child.preferences.configOnTime or 0 + off_pref = child.preferences.configOffWaitTime or 0 + else + on_pref = device.preferences["configOnTime" .. suffix] or 0 + off_pref = device.preferences["configOffWaitTime" .. suffix] or 0 + end + return to_deciseconds(on_pref), to_deciseconds(off_pref) +end + +local function copy_table(source) + local result = {} + for key, value in pairs(source) do + result[key] = value + end + return result +end + +local parent_preference_state = {} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("switch-4inputs-2outputs.yml"), + fingerprinted_endpoint_id = ZIGBEE_ENDPOINTS.INPUT_1, + label = "frient IO Module", + zigbee_endpoints = { + [ZIGBEE_ENDPOINTS.INPUT_1] = { + id = ZIGBEE_ENDPOINTS.INPUT_1, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_2] = { + id = ZIGBEE_ENDPOINTS.INPUT_2, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_3] = { + id = ZIGBEE_ENDPOINTS.INPUT_3, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.INPUT_4] = { + id = ZIGBEE_ENDPOINTS.INPUT_4, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.OUTPUT_1] = { + id = ZIGBEE_ENDPOINTS.OUTPUT_1, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { OnOff.ID, BasicInput.ID }, + }, + [ZIGBEE_ENDPOINTS.OUTPUT_2] = { + id = ZIGBEE_ENDPOINTS.OUTPUT_2, + manufacturer = "frient A/S", + model = "IOMZB-110", + server_clusters = { OnOff.ID, BasicInput.ID }, + }, + }, +}) + + function mock_parent_device:get_model() + return "IOMZB-110" + end + + function mock_parent_device:get_manufacturer() + return "frient A/S" + end + + function mock_parent_device:supports_server_cluster(cluster_id, endpoint_id) + local function endpoint_supports(ep) + if not ep or not ep.server_clusters then return false end + for _, server_cluster in ipairs(ep.server_clusters) do + if server_cluster == cluster_id then + return true + end + end + return false + end + + if endpoint_id ~= nil then + return endpoint_supports(self.zigbee_endpoints[endpoint_id]) + end + + for _, endpoint in pairs(self.zigbee_endpoints) do + if endpoint_supports(endpoint) then + return true + end + end + return false + end + +local mock_output_child_1 = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("frient-io-output-switch.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "frient-io-output-1", + label = "frient IO Module Output 1", + vendor_provided_label = "Output 1", +}) + +local mock_output_child_2 = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("frient-io-output-switch.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "frient-io-output-2", + label = "frient IO Module Output 2", + vendor_provided_label = "Output 2", +}) + +local function reset_preferences() + mock_parent_device.preferences.reversePolarity1 = false + mock_parent_device.preferences.reversePolarity2 = false + mock_parent_device.preferences.reversePolarity3 = false + mock_parent_device.preferences.reversePolarity4 = false + + mock_parent_device.preferences.controlOutput11 = false + mock_parent_device.preferences.controlOutput21 = false + mock_parent_device.preferences.controlOutput12 = false + mock_parent_device.preferences.controlOutput22 = false + mock_parent_device.preferences.controlOutput13 = false + mock_parent_device.preferences.controlOutput23 = false + mock_parent_device.preferences.controlOutput14 = false + mock_parent_device.preferences.controlOutput24 = false + + mock_parent_device.preferences.configOnTime1 = 3 + mock_parent_device.preferences.configOffWaitTime1 = 4 + mock_parent_device.preferences.configOnTime2 = 7 + mock_parent_device.preferences.configOffWaitTime2 = 8 + + mock_output_child_1.preferences.configOnTime = 5 + mock_output_child_1.preferences.configOffWaitTime = 6 + mock_output_child_2.preferences.configOnTime = 0 + mock_output_child_2.preferences.configOffWaitTime = 0 + + parent_preference_state = copy_table(mock_parent_device.preferences) + + local field_keys = { + "frient_io_native_70", + "frient_io_native_71", + "frient_io_native_72", + "frient_io_native_73", + "frient_io_native_74", + "frient_io_native_75", + } + + for _, key in ipairs(field_keys) do + mock_parent_device:set_field(key, nil, { persist = true }) + end + + mock_output_child_1:set_field("frient_io_native_74", nil, { persist = true }) + mock_output_child_2:set_field("frient_io_native_75", nil, { persist = true }) +end + +local function queue_child_info_changed(child, preferences) + local raw = rawget(child, "raw_st_data") + if raw and raw.preferences then + for key, value in pairs(preferences) do + raw.preferences[key] = value + end + end + test.socket.device_lifecycle:__queue_receive(child:generate_info_changed({ preferences = preferences })) +end + +local function queue_parent_info_changed(preferences) + local full_preferences = copy_table(parent_preference_state) + for key, value in pairs(preferences) do + full_preferences[key] = value + end + parent_preference_state = copy_table(full_preferences) + + local raw = rawget(mock_parent_device, "raw_st_data") + if raw and raw.preferences then + for key, value in pairs(full_preferences) do + raw.preferences[key] = value + end + end + + test.socket.device_lifecycle:__queue_receive( + mock_parent_device:generate_info_changed({ preferences = full_preferences }) + ) +end + +local function register_initial_config_expectations() + if test.socket.zigbee and test.socket.zigbee.__set_channel_ordering then + test.socket.zigbee:__set_channel_ordering("relaxed") + end + if test.socket.devices and test.socket.devices.__set_channel_ordering then + test.socket.devices:__set_channel_ordering("relaxed") + end + + local function register_device_configure_expectations() + local configuration = configuration_map.get_device_configuration(mock_parent_device) or {} + local configs_by_cluster = {} + local function add_attribute_config(attribute) + if attribute.configurable ~= false then + configs_by_cluster[attribute.cluster] = configs_by_cluster[attribute.cluster] or {} + table.insert(configs_by_cluster[attribute.cluster], attribute) + end + end + + for _, attribute in ipairs(configuration) do + add_attribute_config(attribute) + end + + local default_configs = switch_defaults.attribute_configurations or {} + for _, attribute in ipairs(default_configs) do + add_attribute_config(attribute) + end + + local cluster_ids = {} + for cluster_id in pairs(configs_by_cluster) do + cluster_ids[#cluster_ids + 1] = cluster_id + end + table.sort(cluster_ids) + + local endpoint_ids = {} + for endpoint_id in pairs(mock_parent_device.zigbee_endpoints) do + endpoint_ids[#endpoint_ids + 1] = endpoint_id + end + table.sort(endpoint_ids) + + for _, cluster_id in ipairs(cluster_ids) do + local attr_configs = configs_by_cluster[cluster_id] + table.sort(attr_configs, function(a, b) + return a.attribute < b.attribute + end) + for _, endpoint_id in ipairs(endpoint_ids) do + local endpoint = mock_parent_device.zigbee_endpoints[endpoint_id] + if endpoint and mock_parent_device:supports_server_cluster(cluster_id, endpoint.id) then + local bind_cmd = device_management.build_bind_request( + mock_parent_device, + cluster_id, + zigbee_test_utils.mock_hub_eui, + endpoint.id + ):to_endpoint(endpoint.id) + bind_cmd.tx_options = data_types.Uint16(0) + test.socket.zigbee:__expect_send({ mock_parent_device.id, bind_cmd }) + for _, attr_config in ipairs(attr_configs) do + local config_cmd = device_management.attr_config(mock_parent_device, attr_config):to_endpoint(endpoint.id) + config_cmd.tx_options = data_types.Uint16(0) + test.socket.zigbee:__expect_send({ mock_parent_device.id, config_cmd }) + end + end + end + end + end + + register_device_configure_expectations() + + local on1, off1 = build_output_timing(mock_parent_device, mock_output_child_1, "1") + local on2, off2 = build_output_timing(mock_parent_device, mock_output_child_2, "2") + + local function enqueue_output_timing_writes() + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, ON_TIME_ATTR, on1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OFF_WAIT_ATTR, off1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_2, ON_TIME_ATTR, on2) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_2, OFF_WAIT_ATTR, off2) }) + end + + -- Device init issues one set of manufacturer-specific writes per output during startup + enqueue_output_timing_writes() + + for _, config in ipairs(INPUT_CONFIGS) do + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_basic_input_polarity_write(mock_parent_device, config.endpoint, false) }) + for _, output_ep in ipairs(config.binds) do + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, config.endpoint, output_ep) }) + end + end +end + +local function expect_init_sequence() + mock_devices_api.__expect_update_device( + mock_parent_device.id, + { deviceId = mock_parent_device.id, provisioningState = "PROVISIONED" } + ) + test.socket.device_lifecycle:__queue_receive({ mock_parent_device.id, "doConfigure" }) +end + +local function expect_switch_registration(device) + test.socket.devices:__expect_send({ + "register_native_capability_attr_handler", + { device_uuid = device.id, capability_id = "switch", capability_attr_id = "switch" }, + }) +end + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + reset_preferences() + register_initial_config_expectations() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_output_child_1) + test.mock_device.add_test_device(mock_output_child_2) + zigbee_test_utils.init_noop_health_check_timer() + --register_initial_config_expectations() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Init configures outputs and routes attribute reports", + function() + expect_init_sequence() + test.wait_for_events() + + test.socket.capability:__set_channel_ordering("relaxed") + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, true):from_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1), + }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.on())) + expect_switch_registration(mock_output_child_1) + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + OnOff.attributes.OnOff:build_test_attr_report(mock_parent_device, false):from_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2), + }) + test.socket.capability:__expect_send(mock_output_child_2:generate_test_message("main", Switch.switch.off())) + expect_switch_registration(mock_output_child_2) + + test.socket.zigbee:__queue_receive({ + mock_parent_device.id, + BasicInput.attributes.PresentValue:build_test_attr_report(mock_parent_device, true):from_endpoint(ZIGBEE_ENDPOINTS.INPUT_3), + }) + test.socket.capability:__expect_send(mock_parent_device:generate_test_message("input3", Switch.switch.on())) + + test.wait_for_events() + + local child1_native = mock_output_child_1:get_field("frient_io_native_74") + assert(child1_native, "expected Output 1 child to register native switch handler") + local child2_native = mock_output_child_2:get_field("frient_io_native_75") + assert(child2_native, "expected Output 2 child to register native switch handler") + local parent_native = mock_parent_device:get_field("frient_io_native_72") + assert(parent_native, "expected parent device to register native switch handler for input 3") + end +) + +test.register_coroutine_test( + "Default responses update state and trigger reads", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + + local on_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.On.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, on_response }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.on())) + + local timed_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.OnWithTimedOff.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, timed_response }) + local read_msg = cluster_base.read_attribute( + mock_parent_device, + data_types.ClusterId(OnOff.ID), + data_types.AttributeId(OnOff.attributes.OnOff.ID) + ) + read_msg.tx_options = data_types.Uint16(0) + read_msg = read_msg:to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, read_msg }) + + local off_response = build_default_response_msg(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OnOff.server.commands.Off.ID) + test.socket.zigbee:__queue_receive({ mock_parent_device.id, off_response }) + test.socket.capability:__expect_send(mock_output_child_1:generate_test_message("main", Switch.switch.off())) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Switch commands drive the correct Zigbee commands", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.capability:__queue_receive({ + mock_output_child_1.id, + { capability = "switch", component = "main", command = "on", args = {} }, + }) + local on1, off1 = build_output_timing(mock_parent_device, mock_output_child_1, "1") + local timed_on = OnOff.server.commands.OnWithTimedOff( + mock_parent_device, + data_types.Uint8(0), + data_types.Uint16(on1), + data_types.Uint16(off1) + ):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, timed_on }) + + test.socket.capability:__queue_receive({ + mock_output_child_1.id, + { capability = "switch", component = "main", command = "off", args = {} }, + }) + local direct_off_output1 = OnOff.server.commands.Off(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_1) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off_output1 }) + + test.socket.capability:__queue_receive({ + mock_output_child_2.id, + { capability = "switch", component = "main", command = "on", args = {} }, + }) + local direct_on = OnOff.server.commands.On(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_on }) + + test.socket.capability:__queue_receive({ + mock_output_child_2.id, + { capability = "switch", component = "main", command = "off", args = {} }, + }) + local direct_off = OnOff.server.commands.Off(mock_parent_device):to_endpoint(ZIGBEE_ENDPOINTS.OUTPUT_2) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off }) + + test.socket.capability:__queue_receive({ + mock_parent_device.id, + { capability = "switch", component = "output1", command = "on", args = {} }, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, timed_on }) + + test.socket.capability:__queue_receive({ + mock_parent_device.id, + { capability = "switch", component = "output2", command = "off", args = {} }, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, direct_off }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Child preference changes send manufacturer writes", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + queue_child_info_changed(mock_output_child_1, { configOnTime = 12, configOffWaitTime = 13 }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, ON_TIME_ATTR, to_deciseconds(12)), + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_client_mfg_write(mock_parent_device, ZIGBEE_ENDPOINTS.OUTPUT_1, OFF_WAIT_ATTR, to_deciseconds(13)), + }) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Parent preference changes manage polarity and binds", + function() + expect_init_sequence() + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + queue_parent_info_changed({ + reversePolarity1 = true, + controlOutput11 = true, + controlOutput21 = true, + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_basic_input_polarity_write(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, true), + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1) }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity1 = true, + controlOutput11 = false, + controlOutput21 = true, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_1, ZIGBEE_ENDPOINTS.OUTPUT_1) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity3 = true, + controlOutput23 = true, + }) + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + build_basic_input_polarity_write(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, true), + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_bind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + + queue_parent_info_changed({ + reversePolarity3 = true, + controlOutput23 = false, + }) + test.socket.zigbee:__expect_send({ mock_parent_device.id, build_unbind(mock_parent_device, ZIGBEE_ENDPOINTS.INPUT_3, ZIGBEE_ENDPOINTS.OUTPUT_2) }) + test.wait_for_events() + end +) + +test.run_registered_tests() From 750cbdf7ea597ffa70730090e635e91c0e54e319 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 28 Jan 2026 11:51:16 -0800 Subject: [PATCH 397/449] WWSTCERT-9848 Netatmo Thermo Hub --- 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 0871a9c0c5..e418d34d3b 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -1354,6 +1354,12 @@ matterManufacturer: vendorId: 0x137F productId: 0x027B deviceProfileName: plug-binary +#Netatmo + - id: "4129/8" + deviceLabel: Netatmo Thermo Hub + vendorId: 0x1021 + productId: 0x0008 + deviceProfileName: matter-bridge #Onvis - id: "5181/4097" deviceLabel: Onvis Smart Plug S4EU From 30ecbf651e2274e656bfcba096f7cbc1b524ad87 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 28 Jan 2026 12:10:13 -0800 Subject: [PATCH 398/449] Revert "Merge pull request #2732 from SmartThingsCommunity/revert/aqara_t1_update" This reverts commit 8b56e69856cd0ef52161b3c1c00d418faf63b548, reversing changes made to 201887c11e3b59e2eb8fffcd7d1d8111957e7d1d. --- .../SmartThings/zigbee-switch/src/aqara-light/init.lua | 9 --------- .../zigbee-switch/src/test/test_aqara_led_bulb.lua | 3 +-- .../zigbee-switch/src/test/test_aqara_light.lua | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua index 06d4bf1cc7..2192716b21 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara-light/init.lua @@ -54,18 +54,9 @@ local function set_level_handler(driver, device, cmd) device:send(Level.commands.MoveToLevelWithOnOff(device, level, dimming_rate)) end -local function init(self, device) - local value = { minimum = 2700, maximum = 6000 } - if device:get_model() == "lumi.light.cwacn1" then - value.maximum = 6500 - end - emit_event_if_latest_state_missing(device, "main", capabilities.colorTemperature, capabilities.colorTemperature.colorTemperatureRange.NAME, capabilities.colorTemperature.colorTemperatureRange(value)) -end - local aqara_light_handler = { NAME = "Aqara Light Handler", lifecycle_handlers = { - init = init, added = device_added, doConfigure = do_configure }, 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 b2f9359850..ce4bb13957 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 @@ -38,7 +38,6 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -52,7 +51,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) 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 940131b3e1..c521ba3e44 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -40,7 +40,6 @@ 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end test.set_test_init_function(test_init) @@ -54,7 +53,7 @@ test.register_coroutine_test( 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.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end ) From 0e3af5a31b4c1c5e02f5b8985af24a51941ed5cb Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 29 Jan 2026 19:56:23 -0800 Subject: [PATCH 399/449] WWSTCERT-10185 ubisys S1 --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index a83204e5f6..1c26d5d6c8 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -600,6 +600,11 @@ zigbeeManufacturer: manufacturer: ubisys model: "D1 (5503)" deviceProfileName: switch-level-power + - id: "ubisys/S1 (5501)" + deviceLabel: Switching Actuator S1 + manufacturer: ubisys + model: "S1 (5501)" + deviceProfileName: switch-power - id: "ubisys/S2 (5502)" deviceLabel: ubisys S2 manufacturer: ubisys @@ -2412,10 +2417,10 @@ zigbeeManufacturer: model: Y-K003-001 deviceProfileName: basic-switch - id: "JNL/Y-K001-001" - deviceLabel: Yanmi Switch (1 Way) + deviceLabel: Yanmi Switch (1 Way) manufacturer: JNL model: Y-K001-001 - deviceProfileName: basic-switch + deviceProfileName: basic-switch - id: "JNL/Y-K002-001" deviceLabel: Yanmi Switch (2 Way) 1 manufacturer: JNL From 117b1e967c07caee24375328af8b19ae7ec0877d Mon Sep 17 00:00:00 2001 From: JanJakubiszyn <101173721+JanJakubiszyn@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:00:48 +0100 Subject: [PATCH 400/449] Adding attribute reading after Offset preference change for instant (#2737) update --- .../SmartThings/matter-switch/src/init.lua | 12 ++ .../test_matter_sensor_offset_preferences.lua | 123 ++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 6655f81e56..0bb9d57ab5 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -81,6 +81,18 @@ function SwitchLifecycleHandlers.info_changed(driver, device, event, args) device_cfg.match_profile(driver, device) end end + + -- instant update of values after offset preference change + for name, info in pairs(device.preferences or {}) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + if name == "tempOffset" then + device:send(clusters.TemperatureMeasurement.attributes.MeasuredValue:read(device)) + elseif name == "humidityOffset" then + device:send(clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(device)) + end + end + end + end function SwitchLifecycleHandlers.device_init(driver, device) 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 new file mode 100644 index 0000000000..de2afe3bd3 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua @@ -0,0 +1,123 @@ +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local utils = require "st.utils" +local dkjson = require "dkjson" +local clusters = require "st.matter.clusters" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("3-button-battery-temperature-humidity.yml"), + matter_version = {hardware = 1, software = 1}, + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.TemperatureMeasurement.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0302, device_type_revision = 1}, + } + }, + { + endpoint_id = 2, + clusters = { + {cluster_id = clusters.RelativeHumidityMeasurement.ID, cluster_type = "BOTH"}, + }, + device_types = { + {device_type_id = 0x0307, device_type_revision = 1}, + } + }, + } +}) + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + + local cluster_subscribe_list = { + clusters.Switch.events.InitialPress, + clusters.Switch.events.LongPress, + clusters.Switch.events.ShortRelease, + clusters.Switch.events.MultiPressComplete, + + clusters.TemperatureMeasurement.attributes.MeasuredValue, + clusters.TemperatureMeasurement.attributes.MinMeasuredValue, + clusters.TemperatureMeasurement.attributes.MaxMeasuredValue, + + clusters.RelativeHumidityMeasurement.attributes.MeasuredValue, + clusters.PowerSource.attributes.BatPercentRemaining + } + + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) + end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + 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" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "3-button-battery-temperature-humidity" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + +end + +test.register_coroutine_test("Read appropriate attribute values after tempOffset preference change", function() + local report = clusters.TemperatureMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,1, 2000) + mock_device.st_store.preferences = {tempOffset = "0"} + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ + value = 20.0, + unit = "C" + }))) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {tempOffset = "5"}})) + test.socket.matter:__expect_send({mock_device.id, clusters.TemperatureMeasurement.attributes.MeasuredValue:read(mock_device)}) + + test.wait_for_events() + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.temperatureMeasurement.temperature({ + value = 20.0, + unit = "C" + }))) +end) + +test.register_coroutine_test("Read appropriate attribute values after humidityOffset preference change", function() + local report = clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device,2, 2000) + mock_device.st_store.preferences = {humidityOffset = "0"} + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ + value = 20 + }))) + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({preferences = {humidityOffset = "5"}})) + test.socket.matter:__expect_send({mock_device.id, clusters.RelativeHumidityMeasurement.attributes.MeasuredValue:read(mock_device)}) + + test.wait_for_events() + + test.socket.matter:__queue_receive({mock_device.id, report}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.relativeHumidityMeasurement.humidity({ + value = 20 + }))) +end) + +test.set_test_init_function(test_init) + +test.run_registered_tests() From 2039f55adfa86380a2335e442e9b3d6a795ddfef Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Tue, 3 Feb 2026 01:09:07 +0900 Subject: [PATCH 401/449] Fixing the Hub Error Log Related to Preference STSE Capability (#2512) --- .../src/aqara/curtain-driver-e1/init.lua | 12 ++++++------ .../zigbee-window-treatment/src/aqara/init.lua | 12 ++++++------ .../src/aqara/roller-shade/init.lua | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 5d4f4d3a37..6fe895ca7d 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -29,10 +29,10 @@ local PRIVATE_CURTAIN_LOCKING_SETTING_ATTRIBUTE_ID = 0x0427 local PRIVATE_CURTAIN_LOCKING_STATUS_ATTRIBUTE_ID = 0x0428 local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseCurtainDirection = capabilities["stse.reverseCurtainDirection"] +local reverseCurtainDirection = "stse.reverseCurtainDirection" local hookLockState = capabilities["stse.hookLockState"] local chargingState = capabilities["stse.chargingState"] -local softTouch = capabilities["stse.softTouch"] +local softTouch = "stse.softTouch" local hookUnlockCommandName = "hookUnlock" local hookLockCommandName = "hookLock" @@ -82,12 +82,12 @@ end local function device_info_changed(driver, device, event, args) if device.preferences ~= nil then - local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection.ID] - local softTouchPrefValue = device.preferences[softTouch.ID] + local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection] + local softTouchPrefValue = device.preferences[softTouch] -- reverse direction if reverseCurtainDirectionPrefValue ~= nil and - reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection.ID] then + reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection] then local raw_value = reverseCurtainDirectionPrefValue and 0x01 or 0x00 device:send(aqara_utils.custom_write_attribute(device, WindowCovering.ID, WindowCovering.attributes.Mode.ID, data_types.Bitmap8, raw_value, nil)) @@ -95,7 +95,7 @@ local function device_info_changed(driver, device, event, args) -- soft touch if softTouchPrefValue ~= nil and - softTouchPrefValue ~= args.old_st_store.preferences[softTouch.ID] then + softTouchPrefValue ~= args.old_st_store.preferences[softTouch] then device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, PRIVATE_CURTAIN_MANUAL_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Boolean, (not softTouchPrefValue))) end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index ae9513734c..87505d2e40 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -11,8 +11,8 @@ local AnalogOutput = clusters.AnalogOutput local Groups = clusters.Groups local deviceInitialization = capabilities["stse.deviceInitialization"] -local reverseCurtainDirection = capabilities["stse.reverseCurtainDirection"] -local softTouch = capabilities["stse.softTouch"] +local reverseCurtainDirection = "stse.reverseCurtainDirection" +local softTouch = "stse.softTouch" local setInitializedStateCommandName = "setInitializedState" local INIT_STATE = "initState" @@ -133,12 +133,12 @@ end local function device_info_changed(driver, device, event, args) if device.preferences ~= nil then - local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection.ID] - local softTouchPrefValue = device.preferences[softTouch.ID] + local reverseCurtainDirectionPrefValue = device.preferences[reverseCurtainDirection] + local softTouchPrefValue = device.preferences[softTouch] -- reverse direction if reverseCurtainDirectionPrefValue ~= nil and - reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection.ID] then + reverseCurtainDirectionPrefValue ~= args.old_st_store.preferences[reverseCurtainDirection] then local raw_value = reverseCurtainDirectionPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.CharString, raw_value)) @@ -152,7 +152,7 @@ local function device_info_changed(driver, device, event, args) -- soft touch if softTouchPrefValue ~= nil and - softTouchPrefValue ~= args.old_st_store.preferences[softTouch.ID] then + softTouchPrefValue ~= args.old_st_store.preferences[softTouch] then local raw_value = softTouchPrefValue and PREF_SOFT_TOUCH_ON or PREF_SOFT_TOUCH_OFF device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.CharString, raw_value)) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index c7b6b9a50a..12b84eb0c8 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -10,7 +10,7 @@ local Basic = clusters.Basic local WindowCovering = clusters.WindowCovering local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] +local reverseRollerShadeDir = "stse.reverseRollerShadeDir" local shadeRotateState = capabilities["stse.shadeRotateState"] local setRotateStateCommandName = "setRotateState" @@ -82,9 +82,9 @@ end local function device_info_changed(driver, device, event, args) if device.preferences ~= nil then - local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] + local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir] if reverseRollerShadeDirPrefValue ~= nil and - reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then + reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir] then local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.CharString, raw_value)) From b17cb987a64864c32872e88a5b09a03e0832c40c Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 21 Jan 2026 11:25:09 -0600 Subject: [PATCH 402/449] Matter Switch: Improve battery profiling This implements a few changes to the way that devices supporting batteries are profiled: * subscribe to PowerSource.AttributeList rather than reading this attribute, to help prevent failed reads from causing issues with device profiling * update the profile within match_profile rather than power_source_attribute_list_handler * use existing structure for handling waiting for profiling data before attempting a profile update --- .../SmartThings/matter-switch/src/init.lua | 5 +- .../switch_handlers/attribute_handlers.lua | 38 +- .../src/switch_utils/device_configuration.lua | 27 +- .../matter-switch/src/switch_utils/fields.lua | 9 +- .../matter-switch/src/switch_utils/utils.lua | 7 + .../test/test_aqara_climate_sensor_w100.lua | 40 +- .../test/test_light_illuminance_motion.lua | 75 +- .../src/test/test_matter_bridge.lua | 8 +- .../src/test/test_matter_button.lua | 555 +++++++----- .../src/test/test_matter_multi_button.lua | 812 +++++++++++------- .../src/test/test_matter_switch.lua | 11 + .../src/test/test_matter_water_valve.lua | 4 + .../src/test/test_multi_switch_mcd.lua | 10 + 13 files changed, 956 insertions(+), 645 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 0bb9d57ab5..e8d1b8330a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -103,10 +103,13 @@ function SwitchLifecycleHandlers.device_init(driver, device) if device:get_field(fields.IS_PARENT_CHILD_DEVICE) then device:set_find_child(switch_utils.find_child) end + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist = true}) + end device:extend_device("subscribe", switch_utils.subscribe) device:subscribe() - -- device energy reporting must be handled cumulatively, periodically, or by both simulatanously. + -- device energy reporting must be handled cumulatively, periodically, or by both simultaneously. -- To ensure a single source of truth, we only handle a device's periodic reporting if cumulative reporting is not supported. if #embedded_cluster_utils.get_endpoints(device, clusters.ElectricalEnergyMeasurement.ID, {feature_bitmap = clusters.ElectricalEnergyMeasurement.types.Feature.CUMULATIVE_ENERGY}) > 0 then 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 e8dfe3f120..ae76709be8 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -319,10 +319,10 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) for i, set_ep_info in pairs(set_topology_eps or {}) do if ib.endpoint_id == set_ep_info.endpoint_id then - -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) local available_endpoints_ids = {} - for _, element in pairs(ib.data.elements) do + for _, element in pairs(ib.data.elements or {}) do table.insert(available_endpoints_ids, element.value) end -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. @@ -344,10 +344,10 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response) local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS) for i, tree_ep_info in pairs(tree_topology_eps or {}) do if ib.endpoint_id == tree_ep_info.endpoint_id then - -- since EP reponse is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table + -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table switch_utils.remove_field_index(device, fields.ELECTRICAL_SENSOR_EPS, i) local associated_endpoints_ids = {} - for _, element in pairs(ib.data.elements) do + for _, element in pairs(ib.data.elements or {}) do table.insert(associated_endpoints_ids, element.value) end -- set the required profile elements ("-power", etc.) to one of these EP IDs for later profiling. @@ -382,29 +382,19 @@ function AttributeHandlers.bat_charge_level_handler(driver, device, ib, response end function AttributeHandlers.power_source_attribute_list_handler(driver, device, ib, response) - local profile_name = "" - - local button_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - 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 - profile_name = "button-battery" - break - elseif attr.value == 0x0E then - profile_name = "button-batteryLevel" + local previous_battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) + for _, attr in ipairs(ib.data.elements or {}) do + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_PERCENTAGE, {persist=true}) break + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID and + device:get_field(fields.profiling_data.BATTERY_SUPPORT) ~= fields.battery_support.BATTERY_PERCENTAGE then -- don't overwrite if percentage support is already detected + device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.BATTERY_LEVEL, {persist=true}) end end - if profile_name ~= "" then - if #button_eps > 1 then - profile_name = string.format("%d-", #button_eps) .. profile_name - end - - if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then - profile_name = profile_name .. "-temperature-humidity" - end - device:try_update_metadata({ profile = profile_name }) + if not previous_battery_support or previous_battery_support ~= device:get_field(fields.profiling_data.BATTERY_SUPPORT) then + device_cfg.match_profile(driver, device) end 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 ffea9efa2e..8542972320 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -123,12 +123,16 @@ function ButtonDeviceConfiguration.update_button_profile(device, default_endpoin if #motion_eps > 0 and (num_button_eps == 3 or num_button_eps == 6) then -- only these two devices are handled profile_name = profile_name .. "-motion" end - local battery_supported = #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) > 0 - if battery_supported then -- battery profiles are configured later, in power_source_attribute_list_handler - device:send(clusters.PowerSource.attributes.AttributeList:read(device)) - else - device:try_update_metadata({profile = profile_name}) + local battery_support = device:get_field(fields.profiling_data.BATTERY_SUPPORT) + if battery_support == fields.battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + elseif battery_support == fields.battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" end + if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then + profile_name = "3-button-battery-temperature-humidity" + end + return profile_name end function ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, button_eps) @@ -238,13 +242,12 @@ function DeviceConfiguration.match_profile(driver, device) end -- initialize the main device card with buttons if applicable - local momemtary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) - if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momemtary_switch_ep_ids) then - ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momemtary_switch_ep_ids) + local momentary_switch_ep_ids = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH}) + if switch_utils.tbl_contains(fields.STATIC_BUTTON_PROFILE_SUPPORTED, #momentary_switch_ep_ids) then + updated_profile = ButtonDeviceConfiguration.update_button_profile(device, default_endpoint_id, #momentary_switch_ep_ids) -- All button endpoints found will be added as additional components in the profile containing the default_endpoint_id. - ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momemtary_switch_ep_ids) - ButtonDeviceConfiguration.configure_buttons(device, momemtary_switch_ep_ids) - return + ButtonDeviceConfiguration.update_button_component_map(device, default_endpoint_id, momentary_switch_ep_ids) + ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) end device:try_update_metadata({ profile = updated_profile, optional_component_capabilities = optional_component_capabilities }) @@ -254,4 +257,4 @@ return { DeviceCfg = DeviceConfiguration, SwitchCfg = SwitchDeviceConfiguration, ButtonCfg = ButtonDeviceConfiguration -} \ No newline at end of file +} diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index ab878698ad..db66c2965c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -139,13 +139,20 @@ SwitchFields.switch_category_vendor_overrides = { SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" --- used in tandem with an EP ID. Stores the required electrical tags "-power", "-energy-powerConsumption", etc. ---- for an Electrical Sensor EP with a "primary" endpoint, used during device profling. +--- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling. SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", + BATTERY_SUPPORT = "__battery_support", +} + +SwitchFields.battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE", } SwitchFields.ENERGY_METER_OFFSET = "__energy_meter_offset" diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index e9809d4e46..da5376f031 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -493,6 +493,13 @@ function utils.subscribe(device) -- 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) + end + if #subscribe_request.info_blocks > 0 then device:send(subscribe_request) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 6371c6be44..d47c666166 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 @@ -1,14 +1,11 @@ -- Copyright © 2024 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local test = require "integration_test" -local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" -local utils = require "st.utils" -local dkjson = require "dkjson" -local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.generated.zap_clusters" -local button_attr = capabilities.button.button +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local uint32 = require "st.matter.data_types.Uint32" -- Mock a 3-button device with temperature and humidity sensor local aqara_mock_device = test.mock_device.build_test_matter_device({ @@ -100,19 +97,20 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ local function configure_buttons() test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false}))) end local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(aqara_mock_device) local cluster_subscribe_list = { + clusters.PowerSource.server.attributes.AttributeList, clusters.PowerSource.server.attributes.BatPercentRemaining, clusters.TemperatureMeasurement.attributes.MeasuredValue, clusters.TemperatureMeasurement.attributes.MinMeasuredValue, @@ -138,23 +136,16 @@ local function test_init() test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "doConfigure" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({aqara_mock_device.id, read_attribute_list}) - configure_buttons() aqara_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - local device_info_copy = utils.deep_copy(aqara_mock_device.raw_st_data) - device_info_copy.profile.id = "3-button-battery-temperature-humidity" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ aqara_mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({aqara_mock_device.id, subscribe_request}) - configure_buttons() end test.set_test_init_function(test_init) local function update_profile() - test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data(aqara_mock_device, 6, {uint32(0x0C)})}) + test.socket.matter:__queue_receive({aqara_mock_device.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + aqara_mock_device, 6, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + configure_buttons() aqara_mock_device:expect_metadata_update({ profile = "3-button-battery-temperature-humidity" }) end @@ -315,7 +306,7 @@ test.register_coroutine_test( clusters.Switch.events.MultiPressComplete:build_test_event_report(aqara_mock_device, 4, {new_position = 0, total_number_of_presses_counted = 2, previous_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.double({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) end ) @@ -335,7 +326,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 3, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -361,7 +352,7 @@ test.register_coroutine_test( clusters.Switch.events.LongPress:build_test_event_report(aqara_mock_device, 5, {new_position = 1}) } ) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.held({state_change = true}))) + test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.held({state_change = true}))) test.socket.matter:__queue_receive( { aqara_mock_device.id, @@ -382,7 +373,7 @@ test.register_coroutine_test( } ) test.socket.capability:__expect_send( - aqara_mock_device:generate_test_message("button1", button_attr.double({state_change = true})) + aqara_mock_device:generate_test_message("button1", capabilities.button.button.double({state_change = true})) ) end ) @@ -466,4 +457,3 @@ test.register_coroutine_test( ) test.run_registered_tests() - 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 24987079fc..a6d660de4e 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 @@ -81,29 +81,35 @@ local function set_color_mode(device, endpoint, color_mode) test.socket.matter:__expect_send({device.id, read_req}) end -local function test_init() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.IlluminanceMeasurement.attributes.MeasuredValue, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.CurrentHue, + clusters.ColorControl.attributes.CurrentSaturation, + clusters.ColorControl.attributes.CurrentX, + clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, + clusters.IlluminanceMeasurement.attributes.MeasuredValue, + clusters.OccupancySensing.attributes.Occupancy +} + +local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) +for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device)) end +end + +local function test_init() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) @@ -111,28 +117,9 @@ end test.set_test_init_function(test_init) local function test_init_x_y_color_mode() - local cluster_subscribe_list = { - clusters.OnOff.attributes.OnOff, - clusters.LevelControl.attributes.CurrentLevel, - clusters.LevelControl.attributes.MaxLevel, - clusters.LevelControl.attributes.MinLevel, - clusters.ColorControl.attributes.CurrentHue, - clusters.ColorControl.attributes.CurrentSaturation, - clusters.ColorControl.attributes.CurrentX, - clusters.ColorControl.attributes.CurrentY, - clusters.ColorControl.attributes.ColorMode, - clusters.ColorControl.attributes.ColorTemperatureMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, - clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, - clusters.IlluminanceMeasurement.attributes.MeasuredValue, - clusters.OccupancySensing.attributes.Occupancy - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, cluster in ipairs(cluster_subscribe_list) do - if i > 1 then - subscribe_request:merge(cluster:subscribe(mock_device)) - end - end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) 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 c696815a3e..20e2545349 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -61,15 +61,15 @@ local mock_bridge = test.mock_device.build_test_matter_device({ local function test_init_mock_bridge() test.mock_device.add_test_device(mock_bridge) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) + mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end test.register_coroutine_test( "Profile should not change for devices with aggregator device type (bridges)", function() - test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "init" }) - test.socket.device_lifecycle:__queue_receive({ mock_bridge.id, "doConfigure" }) - mock_bridge:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { test_init = test_init_mock_bridge } ) 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 58ba831074..f84c29c4c5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -4,15 +4,43 @@ local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" - local clusters = require "st.matter.clusters" local button_attr = capabilities.button.button local utils = require "st.utils" local dkjson = require "dkjson" local uint32 = require "st.matter.data_types.Uint32" ---mock the actual device local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("button.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 = 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 + } + } + } +}) + +local mock_device_battery = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("button-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, matter_version = {hardware = 1, software = 1}, @@ -47,27 +75,34 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, -} - -local function configure_buttons() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) +local function expect_configure_buttons(device) + test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) +end + +local function update_profile() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 1, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "button-battery" }) end local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + 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.disable_startup_messages() + test.mock_device.add_test_device(mock_device) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) @@ -75,154 +110,223 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() - test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) - configure_buttons() - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "buttons-battery" - 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 + +local function test_init_battery() + local CLUSTER_SUBSCRIBE_LIST_BATTERY = { + clusters.PowerSource.server.attributes.AttributeList, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_BATTERY[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST_BATTERY) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + + 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 test.set_test_init_function(test_init) +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_battery.raw_st_data) + device_info_copy.profile.id = "buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "infoChanged", device_info_json }) + -- due to the AttributeList being processed in update_profile, setting profiling_data.BATTERY_SUPPORT, + -- subsequent subscriptions will not include AttributeList. + local UPDATED_CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local updated_subscribe_request = UPDATED_CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(UPDATED_CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then updated_subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + test.socket.matter:__expect_send({mock_device_battery.id, updated_subscribe_request}) + expect_configure_buttons(mock_device_battery) + end, + { test_init = test_init_battery } +) + +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + 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 } +) + test.register_message_test( "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_message_test( "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } -} ) test.register_message_test( "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event - -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, } ) test.register_message_test( "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 1, {previous_position = 1} - ) - } - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 1, {previous_position = 1} + ) + } + }, } ) @@ -291,124 +395,129 @@ test.register_message_test( test.register_message_test( "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + } ) test.register_message_test( "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See the multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} -) - -test.register_message_test( - "Handle received BatPercentRemaining from device.", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device, 1, 150 - ), - }, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 1, previous_position = 0} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See the multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 1, {new_position = 1, total_number_of_presses_counted = 4, previous_position = 0} + ) + } }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) }, } ) +local function reset_battery_profiling_info() + local fields = require "switch_utils.fields" + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +end + test.register_coroutine_test( - "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(12)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) } ) - mock_device:expect_metadata_update({ profile = "button-battery" }) end ) test.register_coroutine_test( - "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", function() + reset_battery_profiling_info() + test.wait_for_events() test.socket.matter:__queue_receive( { mock_device.id, - clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32(10)}) + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatChargeLevel.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 1, {uint32( + clusters.PowerSource.attributes.BatPercentRemaining.ID + )}) } ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "button-battery" }) end ) --- run the tests test.run_registered_tests() 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 301fb36cf0..33002a8203 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 @@ -6,16 +6,15 @@ local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local utils = require "st.utils" local dkjson = require "dkjson" - local clusters = require "st.matter.generated.zap_clusters" local button_attr = capabilities.button.button +local uint32 = require "st.matter.data_types.Uint32" --- Mock a 5-button device using endpoints non-consecutive endpoints local mock_device = test.mock_device.build_test_matter_device( { - profile = t_utils.get_profile_definition("5-button-battery.yml"), + profile = t_utils.get_profile_definition("5-button.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, - matter_version = {hardware = 1, sofrware = 1}, + matter_version = {hardware = 1, software = 1}, endpoints = { { endpoint_id = 0, @@ -32,7 +31,6 @@ local mock_device = test.mock_device.build_test_matter_device( feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, cluster_type = "SERVER" }, - {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} }, device_types = { {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch @@ -95,44 +93,135 @@ local mock_device = test.mock_device.build_test_matter_device( }, }) --- add device for each mock device -local CLUSTER_SUBSCRIBE_LIST ={ - clusters.PowerSource.server.attributes.BatPercentRemaining, - clusters.Switch.server.events.InitialPress, - clusters.Switch.server.events.LongPress, - clusters.Switch.server.events.ShortRelease, - clusters.Switch.server.events.MultiPressComplete, -} +local mock_device_battery = test.mock_device.build_test_matter_device( + { + profile = t_utils.get_profile_definition("5-button-battery.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = {}, + device_types = { + {device_type_id = 0x0016, device_type_revision = 1}, -- RootNode + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + }, + {cluster_id = clusters.PowerSource.ID, cluster_type = "SERVER", feature_map = clusters.PowerSource.types.PowerSourceFeature.BATTERY} + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 20, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 30, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 50, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 60, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_MULTI_PRESS | + clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS, + cluster_type = "SERVER" + }, + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + }, + }) -local function expect_configure_buttons() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) +local function expect_configure_buttons(device) + test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 50)}) + test.socket.capability:__expect_send(device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) + test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 60)}) + test.socket.capability:__expect_send(device:generate_test_message("button5", button_attr.pushed({state_change = false}))) +end + +local function update_profile() + test.socket.matter:__queue_receive({mock_device_battery.id, clusters.PowerSource.attributes.AttributeList:build_test_report_data( + mock_device_battery, 10, {uint32(clusters.PowerSource.attributes.BatPercentRemaining.ID)} + )}) + expect_configure_buttons(mock_device_battery) + mock_device_battery:expect_metadata_update({ profile = "5-button-battery" }) end -- All messages queued and expectations set are done before the driver is actually run local function test_init() + local CLUSTER_SUBSCRIBE_LIST = { + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + 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 + -- we dont want the integration test framework to generate init/doConfigure, we are doing that here -- so we can set the proper expectations on those events. test.disable_startup_messages() test.mock_device.add_test_device(mock_device) -- make sure the cache is populated -- added sets a bunch of fields on the device, and calls init - 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.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) @@ -141,69 +230,140 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) --doConfigure sets the provisioning state to provisioned - test.socket.matter:__expect_send({mock_device.id, clusters.PowerSource.attributes.AttributeList:read()}) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - expect_configure_buttons() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end - -- simulate the profile change update taking affect and the device info changing - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "5-buttons-battery" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - expect_configure_buttons() +local function test_init_battery() + local CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.AttributeList, + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + + local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_battery) + + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end + test.set_test_init_function(test_init) +test.register_coroutine_test( + "Simulate the profile change update taking affect and the device info changing", + function() + test.socket.matter:__set_channel_ordering("relaxed") + update_profile() + test.wait_for_events() + local device_info_copy = utils.deep_copy(mock_device_battery.raw_st_data) + device_info_copy.profile.id = "5-buttons-battery" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "infoChanged", device_info_json }) + -- due to the AttributeList being processed in update_profile, setting profiling_data.BATTERY_SUPPORT, + -- subsequent subscriptions will not include AttributeList. + local UPDATED_CLUSTER_SUBSCRIBE_LIST = { + clusters.PowerSource.server.attributes.BatPercentRemaining, + clusters.Switch.server.events.InitialPress, + clusters.Switch.server.events.LongPress, + clusters.Switch.server.events.ShortRelease, + clusters.Switch.server.events.MultiPressComplete, + } + local updated_subscribe_request = UPDATED_CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_device_battery) + for i, clus in ipairs(UPDATED_CLUSTER_SUBSCRIBE_LIST) do + if i > 1 then updated_subscribe_request:merge(clus:subscribe(mock_device_battery)) end + end + test.socket.matter:__expect_send({mock_device_battery.id, updated_subscribe_request}) + expect_configure_buttons(mock_device_battery) + end, + { test_init = test_init_battery } +) + +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + update_profile() + test.socket.matter:__queue_receive( + { + mock_device_battery.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device_battery, 10, 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 } +) + test.register_message_test( "Handle single press sequence, no hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_message_test( "Handle single press sequence for short release-supported button", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 20, {new_position = 1} --move to position 1? - ), - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 20, {previous_position = 0} --move to position 1? - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 20, {new_position = 1} --move to position 1 + ), + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 20, {previous_position = 0} --move to position 1 + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button2", button_attr.pushed({state_change = true})) --should send initial press } -} ) test.register_coroutine_test( @@ -360,117 +520,117 @@ test.register_coroutine_test( test.register_message_test( "Handle single press sequence, with hold", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) --should send initial press + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) } -} ) test.register_message_test( "Handle release after short press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.ShortRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, - { -- this is a double event because the test device in this test shouldn't support the above event - -- but we handle it anyway - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.ShortRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, + { -- this is a double event because the test device in this test shouldn't support the above event + -- but we handle it anyway + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, } ) test.register_message_test( "Handle release after long press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ), - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongRelease:build_test_event_report( - mock_device, 10, {previous_position = 1} - ) - } - }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ), + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongRelease:build_test_event_report( + mock_device, 10, {previous_position = 1} + ) + } + }, } ) @@ -539,161 +699,136 @@ test.register_message_test( test.register_message_test( "Handle double press", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) - }, - -} + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 2, previous_position = 0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.double({state_change = true})) + }, + } ) test.register_message_test( "Handle multi press for 4 times", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 10, {new_position = 1} - ) - } - }, - { -- again, on a device that reports that it supports double press, this event - -- will not be generated. See a multi-button test file for that case - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) - }, - -} -) - -test.register_message_test( - "Receiving a max press attribute of 2 should emit correct event", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.attributes.MultiPressMax:build_test_report_data( - mock_device, 50, 2 - ) - }, - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button4", - capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) - }, - } + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 10, {new_position = 1} + ) + } + }, + { -- again, on a device that reports that it supports double press, this event + -- will not be generated. See a multi-button test file for that case + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 10, {new_position = 1, total_number_of_presses_counted = 4, previous_position=0} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", button_attr.pushed_4x({state_change = true})) + }, + } ) test.register_message_test( - "Handle received BatPercentRemaining from device.", { + "Receiving a max press attribute of 2 should emit correct event", { { channel = "matter", direction = "receive", message = { mock_device.id, - clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( - mock_device, 10, 150 - ), + clusters.Switch.attributes.MultiPressMax:build_test_report_data( + mock_device, 50, 2 + ) }, }, { channel = "capability", direction = "send", - message = mock_device:generate_test_message( - "main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5)) - ), + message = mock_device:generate_test_message("button4", + capabilities.button.supportedButtonValues({"pushed", "double"}, {visibility = {displayed = false}})) }, } ) - test.register_message_test( "Handle a long press including MultiPressComplete", { - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} - ) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) - }, - { - channel = "matter", - direction = "receive", - message = { - mock_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} - ) + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_device, 60, {new_position = 1} + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("button5", button_attr.held({state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + ) + } } + -- no double event } - -- no double event -} ) test.register_message_test( @@ -704,7 +839,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -714,7 +849,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.LongPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -729,7 +864,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.InitialPress:build_test_event_report( - mock_device, 60, {new_position = 1} + mock_device, 60, {new_position = 1} ) } }, @@ -739,7 +874,7 @@ test.register_message_test( message = { mock_device.id, clusters.Switch.events.MultiPressComplete:build_test_event_report( - mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} + mock_device, 60, {new_position = 0, total_number_of_presses_counted = 1, previous_position=0} ) } }, @@ -750,5 +885,60 @@ test.register_message_test( } } ) --- run the tests + +local function reset_battery_profiling_info() + local fields = require "switch_utils.fields" + mock_device:set_field(fields.profiling_data.BATTERY_SUPPORT, fields.battery_support.NO_BATTERY, {persist=true}) +end + +test.register_coroutine_test( + "Test profile does not change to button-battery when battery percent remaining attribute (attribute ID 12) is not available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32(10)}) + } + ) + end +) + +test.register_coroutine_test( + "Test profile change to button-batteryLevel when battery percent remaining attribute (attribute ID 14) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32( + clusters.PowerSource.attributes.BatChargeLevel.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button-batteryLevel" }) + end +) + +test.register_coroutine_test( + "Test profile change to button-battery when battery percent remaining attribute (attribute ID 12) is available", + function() + reset_battery_profiling_info() + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + clusters.PowerSource.attributes.AttributeList:build_test_report_data(mock_device, 10, {uint32( + clusters.PowerSource.attributes.BatPercentRemaining.ID + )}) + } + ) + expect_configure_buttons(mock_device) + mock_device:expect_metadata_update({ profile = "5-button-battery" }) + end +) + test.run_registered_tests() 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 b57c416434..86edf45403 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -153,6 +153,11 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- note that since disable_startup_messages is not explicitly called here, + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENT_HUE_AND_CURRENT_SATURATION) @@ -166,6 +171,9 @@ local function test_init_x_y_color_mode() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) set_color_mode(mock_device, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) @@ -178,6 +186,9 @@ local function test_init_no_hue_sat() subscribe_request:merge(cluster:subscribe(mock_device_no_hue_sat)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device_no_hue_sat.id, "added" }) + test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) + test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request}) test.mock_device.add_test_device(mock_device_no_hue_sat) set_color_mode(mock_device_no_hue_sat, 1, clusters.ColorControl.types.ColorMode.CURRENTX_AND_CURRENTY) 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 6129dfd116..da74443896 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 @@ -57,6 +57,10 @@ local function test_init() subscribe_request:merge(cluster:subscribe(mock_device)) end end + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end 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 75e88b5fce..774e6bfa52 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 @@ -145,6 +145,10 @@ local function test_init_mock_3switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch) + test.socket.device_lifecycle:__queue_receive({ mock_3switch.id, "added" }) + test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) + + -- the following subscribe is due to the init event sent by the test framework. test.socket.matter:__expect_send({mock_3switch.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch) end @@ -157,6 +161,9 @@ local function test_init_mock_2switch() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_2switch) + test.socket.device_lifecycle:__queue_receive({ mock_2switch.id, "added" }) + test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) + test.socket.matter:__expect_send({mock_2switch.id, subscribe_request}) test.mock_device.add_test_device(mock_2switch) end @@ -169,6 +176,9 @@ local function test_init_mock_3switch_non_sequential() } test.socket.matter:__set_channel_ordering("relaxed") local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch_non_sequential) + test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "added" }) + test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) + test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request}) test.mock_device.add_test_device(mock_3switch_non_sequential) end From 3bbeb209cfcc8cab687b25f5aa7e312fc565eedc Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Wed, 4 Feb 2026 11:58:53 -0600 Subject: [PATCH 403/449] Add Ledvance child device vendor override --- .../matter-switch/src/switch_utils/fields.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index db66c2965c..d23c588b7c 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -93,10 +93,6 @@ SwitchFields.updated_fields = { } SwitchFields.vendor_overrides = { - [0x1321] = { -- SONOFF_MANUFACTURER_ID - [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, - [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, - }, [0x115F] = { -- AQARA_MANUFACTURER_ID [0x1006] = { ignore_combo_switch_button = true }, -- 3 Buttons(Generic Switch), 1 Channel (Dimmable Light) [0x100A] = { ignore_combo_switch_button = true }, -- 1 Buttons(Generic Switch), 1 Channel (Dimmable Light) @@ -105,6 +101,14 @@ SwitchFields.vendor_overrides = { [0x117C] = { -- IKEA_MANUFACTURER_ID [0x8000] = { is_ikea_scroll = true } }, + [0x1189] = { -- LEDVANCE_MANUFACTURER_ID + [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, + [0x0892] = { target_profile = "switch-binary", initial_profile = "light-binary" }, + }, + [0x1321] = { -- SONOFF_MANUFACTURER_ID + [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, + }, } SwitchFields.switch_category_vendor_overrides = { From 935c0018c108bb324be2b39fee96711f05070735 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:22:16 -0600 Subject: [PATCH 404/449] Remove unneeded override --- drivers/SmartThings/matter-switch/src/switch_utils/fields.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index d23c588b7c..dcc3403780 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -103,7 +103,6 @@ SwitchFields.vendor_overrides = { }, [0x1189] = { -- LEDVANCE_MANUFACTURER_ID [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, - [0x0892] = { target_profile = "switch-binary", initial_profile = "light-binary" }, }, [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, From 1ecc63b37bac58b66d051ba511d54e37ca49cbad Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:49:18 +0100 Subject: [PATCH 405/449] Merge pull request #2431 from marcintyminski/Add-support-for-frient-Smart-Sirens WWSTCERT-10049/10052/10055 Add support to smart sirens --- .../SmartThings/zigbee-siren/fingerprints.yml | 20 +- .../frient-siren-battery-source-tamper.yml | 53 + .../profiles/frient-siren-battery-source.yml | 51 + .../zigbee-siren/src/frient/init.lua | 406 ++++++- drivers/SmartThings/zigbee-siren/src/init.lua | 25 +- .../src/test/test_frient_siren.lua | 1046 +++++++++++++++-- .../src/test/test_frient_siren_tamper.lua | 1029 ++++++++++++++++ .../src/test/test_zigbee_siren.lua | 23 + 8 files changed, 2517 insertions(+), 136 deletions(-) create mode 100644 drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml create mode 100644 drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml create mode 100644 drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua diff --git a/drivers/SmartThings/zigbee-siren/fingerprints.yml b/drivers/SmartThings/zigbee-siren/fingerprints.yml index d40b7efb9a..57b192b294 100644 --- a/drivers/SmartThings/zigbee-siren/fingerprints.yml +++ b/drivers/SmartThings/zigbee-siren/fingerprints.yml @@ -4,16 +4,26 @@ zigbeeManufacturer : manufacturer: ClimaxTechnology model: SRAC_00.00.00.16TC deviceProfileName: switch-alarm-generic-siren-7 + - id: "frient/SIRZB-110" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-110 + deviceProfileName: frient-siren-battery-source-tamper + - id: "frient/SIRZB-111" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-111 + deviceProfileName: frient-siren-battery-source + - id: "frient/SIRZB-112" + deviceLabel: frient Smart Siren + manufacturer: frient A/S + model: SIRZB-112 + deviceProfileName: frient-siren-battery-source-tamper - id : Heiman/WarningDevice deviceLabel : HEIMAN Siren manufacturer : Heiman model : WarningDevice deviceProfileName : switch-alarm-generic-siren-7 - - id : frient/SIRZB-110 - deviceLabel : frient Siren - manufacturer : frient A/S - model : SIRZB-110 - deviceProfileName : switch-alarm-generic-siren-7 - id: "Sercomm Corp./SZ-SRN12N" deviceLabel: SmartThings Siren manufacturer: Sercomm Corp. diff --git a/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml new file mode 100644 index 0000000000..6c7e991e45 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source-tamper.yml @@ -0,0 +1,53 @@ +name: frient-siren-battery-source-tamper +components: + - id: main + capabilities: + - id: alarm + version: 1 + - id: tone + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + - id: tamperAlert + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Siren + - id: SirenVolume + capabilities: + - id: mode + version: 1 + - id: SirenVoice + capabilities: + - id: mode + version: 1 + - id: SquawkVolume + capabilities: + - id: mode + version: 1 + - id: SquawkVoice + capabilities: + - id: mode + version: 1 + - id: WarningDuration + capabilities: + - id: mode + version: 1 +preferences: + - title: "Max alarm duration (s)" + name: maxWarningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 +metadata: + mnmn: SmartThings + vid: SmartThings-smartthings-frient_Siren_Tamper \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml new file mode 100644 index 0000000000..5248db2b54 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/profiles/frient-siren-battery-source.yml @@ -0,0 +1,51 @@ +name: frient-siren-battery-source +components: + - id: main + capabilities: + - id: alarm + version: 1 + - id: tone + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Siren + - id: SirenVolume + capabilities: + - id: mode + version: 1 + - id: SirenVoice + capabilities: + - id: mode + version: 1 + - id: SquawkVolume + capabilities: + - id: mode + version: 1 + - id: SquawkVoice + capabilities: + - id: mode + version: 1 + - id: WarningDuration + capabilities: + - id: mode + version: 1 +preferences: + - title: "Max alarm duration (s)" + name: maxWarningDuration + description: "After how many seconds should the alarm turn off" + required: false + preferenceType: integer + definition: + minimum: 0 + maximum: 65534 + default: 240 +metadata: + mnmn: SmartThings + vid: SmartThings-smartthings-frient_Siren \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/frient/init.lua b/drivers/SmartThings/zigbee-siren/src/frient/init.lua index 085b8ecd79..78738ca9a8 100644 --- a/drivers/SmartThings/zigbee-siren/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/frient/init.lua @@ -11,87 +11,417 @@ -- 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 data_types = require "st.zigbee.data_types" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" --ZCL local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local Basic = zcl_clusters.Basic local IASWD = zcl_clusters.IASWD -local SirenConfiguration = IASWD.types.SirenConfiguration +local IASZone = zcl_clusters.IASZone local IaswdLevel = IASWD.types.IaswdLevel +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local SquawkMode = IASWD.types.SquawkMode +local WarningMode = IASWD.types.WarningMode +local PowerConfiguration = zcl_clusters.PowerConfiguration --capability local capabilities = require "st.capabilities" local alarm = capabilities.alarm -local switch = capabilities.switch local ALARM_COMMAND = "alarmCommand" -local ALARM_LAST_DURATION = "lastDuration" -local ALARM_MAX_DURATION = "maxDuration" +local ALARM_DURATION = "warningDuration" +local SIREN_FIXED_ENDIAN_SW_VERSION = "010903" -local ALARM_DEFAULT_MAX_DURATION = 0x00B4 -local ALARM_STROBE_DUTY_CYCLE = 00 +local DEFAULT_MAX_WARNING_DURATION = 0x00F0 +local PRIMARY_SW_VERSION = "primary_sw_version" +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 +local IASZONE_ENDPOINT = 0x2B local alarm_command = { OFF = 0, SIREN = 1, - STROBE = 2, - BOTH = 3 } -local send_siren_command = function(device) - local max_duration = device:get_field(ALARM_MAX_DURATION) - local warning_duration = max_duration and max_duration or ALARM_DEFAULT_MAX_DURATION - local duty_cycle = ALARM_STROBE_DUTY_CYCLE +local IASZone_configuration = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 6*60*60, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1 + } +} - device:set_field(ALARM_LAST_DURATION, warning_duration, {persist = true}) +local SQUAWK_VOICE_MAP = { + ["Armed"] = 0, + ["Disarmed"] = 1 +} - local siren_configuration = SirenConfiguration(0xC1) +local VOLUME_MAP = { + ["Low"] = 0, + ["Medium"] = 1, + ["High"] = 2, + ["Very High"] = 3 +} +local SIREN_VOICE_MAP = { + ["Burglar"] = 1, + ["Fire"] = 2, + ["Emergency"] = 3, + ["Panic"] = 4, + ["Panic Fire"] = 5, + ["Panic Emergency"] = 6 +} - device:send( - IASWD.server.commands.StartWarning( - device, - siren_configuration, - data_types.Uint16(warning_duration), - data_types.Uint8(duty_cycle), - data_types.Enum8(IaswdLevel.LOW_LEVEL) +local WARNING_DURATION_MAP = { + ["5 seconds"] = 5, + ["10 seconds"] = 10, + ["15 seconds"] = 15, + ["20 seconds"] = 20, + ["25 seconds"] = 25, + ["30 seconds"] = 30, + ["40 seconds"] = 40, + ["50 seconds"] = 50, + ["1 minute"] = 60, + ["2 minutes"] = 120, + ["3 minutes"] = 180, + ["4 minutes"] = 240, + ["5 minutes"] = 300, + ["10 minutes"] = 600 +} + +local MODEL_DEVICE_PROFILE_MAP = { + ["SIRZB-110"] = "frient-siren-battery-source-tamper", + ["SIRZB-111"] = "frient-siren-battery-source" +} + +local BATTERY_CONFIG_APPLIED_KEY = "_frient_battery_config_applied" + +local function get_current_max_warning_duration(device) + return device.preferences.maxWarningDuration == nil and DEFAULT_MAX_WARNING_DURATION or device.preferences.maxWarningDuration +end + +local function get_warning_duration(device) + -- User may select one of predefine modes on the UI or input duration in preferences + -- every time ALARM_DURATION is updated + local selected_duration = device:get_field(ALARM_DURATION) + local current_max_warning_duration = get_current_max_warning_duration(device) + + local warning_duration + if selected_duration == nil or selected_duration > current_max_warning_duration then + warning_duration = current_max_warning_duration + else + warning_duration = selected_duration + end + return warning_duration +end + +local function configure_battery_handling_based_on_fw(driver, device) + local sw_version = device:get_field(PRIMARY_SW_VERSION) + local applied_state = device:get_field(BATTERY_CONFIG_APPLIED_KEY) + + if sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION then + if applied_state ~= "voltage" then + -- Old firmware - does not support BatteryPercentageRemaining attribute, use battery defaults (voltage-based) + battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device) + device:set_field(BATTERY_CONFIG_APPLIED_KEY, "voltage", { persist = true }) + return true + end + else + if applied_state ~= "percentage" then + -- New firmware - supports BatteryPercentageRemaining, remove voltage monitoring + device:remove_configured_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryVoltage.ID) + device:remove_monitored_attribute(PowerConfiguration.ID, PowerConfiguration.attributes.BatteryVoltage.ID) + device:set_field(BATTERY_CONFIG_APPLIED_KEY, "percentage", { persist = true }) + return true + end + end + + return false +end + +local function device_init(driver, device) + for _, attribute in ipairs(IASZone_configuration) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end +end + +local function device_added (driver, device) + for comp_name, comp in pairs(device.profile.components) do + if comp_name ~= "main" then + if comp_name == "SirenVoice" then + device:emit_component_event(comp, capabilities.mode.supportedModes({"Burglar", "Fire", "Emergency", "Panic","Panic Fire","Panic Emergency" }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Burglar", "Fire", "Emergency", "Panic","Panic Fire","Panic Emergency" }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Burglar")) + elseif comp_name == "SquawkVoice" then + device:emit_component_event(comp, capabilities.mode.supportedModes({"Armed", "Disarmed"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Armed", "Disarmed"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Armed")) + elseif comp_name == "WarningDuration" then + device:emit_component_event(comp, capabilities.mode.supportedModes({ + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({ + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("4 minutes")) + else + device:emit_component_event(comp, capabilities.mode.supportedModes({"Low", "Medium", "High", "Very High"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.supportedArguments({"Low", "Medium", "High", "Very High"}, {visibility = {displayed = false}})) + device:emit_component_event(comp, capabilities.mode.mode("Very High")) + end + end + end + + device:emit_event(capabilities.alarm.alarm.off()) + + if(device:supports_capability(capabilities.tamperAlert)) then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function do_refresh(driver, device) + device:refresh() + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(IASZONE_ENDPOINT)) + + -- Check if we have the software version + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end +end + +local function do_configure(driver, device) + local maxWarningDuration = get_current_max_warning_duration(device) + device:set_field(ALARM_DURATION, maxWarningDuration , { persist = true}) + device:send(IASWD.attributes.MaxDuration:write(device, maxWarningDuration):to_endpoint(0x2B)) + + -- Check if we have the software version + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + else + configure_battery_handling_based_on_fw(driver, device) + end + + -- handle frient sirens that were already connected and using old device profile + if (not device:supports_capability_by_id(capabilities.mode.ID)) then + device:try_update_metadata({profile = MODEL_DEVICE_PROFILE_MAP[device:get_model()]}) + end + + device.thread:call_with_delay(5, function() + do_refresh(driver, device) + end) + device:configure() +end + +local function primary_sw_version_attr_handler(driver, device, value, zb_rx) + local primary_sw_version = value.value:gsub('.', function (c) return string.format('%02x', string.byte(c)) end) + device:set_field(PRIMARY_SW_VERSION, primary_sw_version, {persist = true}) + local config_changed = configure_battery_handling_based_on_fw(driver, device) + if config_changed then + device.thread:call_with_delay(1, function() + device:configure() + end) + end + device.thread:call_with_delay(config_changed and 2 or 1, function() + do_refresh(driver, device) + end) +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + if device:supports_capability(capabilities.tamperAlert) then + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + zone_status:is_tamper_set() and capabilities.tamperAlert.tamper.detected() or capabilities.tamperAlert.tamper.clear() ) + end + device:emit_event_for_endpoint( + zb_rx.address_header.src_endpoint.value, + zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains() + ) +end + +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + local zone_status = zb_rx.body.zcl_body.zone_status + generate_event_from_zone_status(driver, device, zone_status, zb_rx) +end + +local function send_siren_command(device, warning_mode, warning_siren_level) + -- Check if we have the software version first + local sw_version = device:get_field(PRIMARY_SW_VERSION) + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end + + local warning_duration = get_warning_duration(device) + + local siren_configuration + + if (sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION) then + -- Old frient firmware, the endian format is reversed + local siren_config_value = (warning_siren_level << 6) | warning_mode + siren_configuration = SirenConfiguration(siren_config_value) + else + siren_configuration = SirenConfiguration(0x00) + siren_configuration:set_warning_mode(warning_mode) + siren_configuration:set_siren_level(warning_siren_level) + end + + device:send( + IASWD.server.commands.StartWarning( + device, + siren_configuration, + data_types.Uint16(warning_duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) ) end -local siren_switch_both_handler = function(driver, device, command) - device:set_field(ALARM_COMMAND, alarm_command.BOTH, {persist = true}) - send_siren_command(device) +local function siren_switch_off_handler(driver, device, command) + device:set_field(ALARM_COMMAND, alarm_command.OFF, {persist = true}) + send_siren_command(device, WarningMode.STOP, IaswdLevel.LOW_LEVEL) end -local siren_alarm_siren_handler = function(driver, device, command) +local function siren_alarm_siren_handler(driver, device, command) device:set_field(ALARM_COMMAND, alarm_command.SIREN, {persist = true}) - send_siren_command(device) + + -- delay is needed to allow st automations get updated fields when mode, volume, voice is set sequentially + device.thread:call_with_delay(1, function() + local sirenVoice_msg = device:get_latest_state("SirenVoice", capabilities.mode.ID, capabilities.mode.mode.NAME) + local sirenVolume_msg = device:get_latest_state("SirenVolume", capabilities.mode.ID, capabilities.mode.mode.NAME) + send_siren_command(device,sirenVoice_msg == nil and WarningMode.BURGLAR or SIREN_VOICE_MAP[sirenVoice_msg] , sirenVolume_msg == nil and IaswdLevel.VERY_HIGH_LEVEL or VOLUME_MAP[sirenVolume_msg]) + end) + + local warningDurationDelay = get_warning_duration(device) + + device.thread:call_with_delay(warningDurationDelay, function() -- Send command to switch from siren to off in the app when the siren is done + if(device:get_field(ALARM_COMMAND) == alarm_command.SIREN) then + siren_switch_off_handler(driver, device, command) + end + end) +end + +local function send_squawk_command(device, squawk_mode, squawk_siren_level) + -- Check if we have the software version first + local sw_version = device:get_field(PRIMARY_SW_VERSION) + + if ((sw_version == nil) or (sw_version == "")) then + device:send(cluster_base.read_manufacturer_specific_attribute(device, Basic.ID, DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, DEVELCO_MANUFACTURER_CODE)) + end + + local squawk_configuration + + if (sw_version and sw_version < SIREN_FIXED_ENDIAN_SW_VERSION) then + -- Old frient firmware, the endian format is reversed + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + squawk_configuration = SquawkConfiguration(squawk_config_value) + else + squawk_configuration = SquawkConfiguration(0x00) + squawk_configuration:set_squawk_mode(squawk_mode) + squawk_configuration:set_squawk_level(squawk_siren_level) + end + + device:send( + IASWD.server.commands.Squawk( + device, + squawk_configuration + ) + ) +end + +local function siren_tone_beep_handler(driver, device, command) + device.thread:call_with_delay(1, function () + local squawkVolume_msg = device:get_latest_state("SquawkVolume", capabilities.mode.ID, capabilities.mode.mode.NAME) + local squawkVoice_msg = device:get_latest_state("SquawkVoice", capabilities.mode.ID, capabilities.mode.mode.NAME) + + send_squawk_command(device, SQUAWK_VOICE_MAP[squawkVoice_msg] or SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED,VOLUME_MAP[squawkVolume_msg] or IaswdLevel.VERY_HIGH_LEVEL) + end ) end -local siren_alarm_strobe_handler = function(driver, device, command) - device:set_field(ALARM_COMMAND, alarm_command.STROBE, {persist = true}) - send_siren_command(device) +local function info_changed(driver, device, event, args) + for name, info in pairs(device.preferences) do + if (device.preferences[name] ~= nil and args.old_st_store.preferences[name] ~= device.preferences[name]) then + local input = device.preferences[name] + if (name == "maxWarningDuration") then + device:send(IASWD.attributes.MaxDuration:write(device, tonumber(input))) + end + end + end end -local siren_switch_on_handler = function(driver, device, command) - siren_switch_both_handler(driver, device, command) +local function siren_mode_handler(driver, device, command) + local mode_set = command.args.mode + local component = command.component + local compObj = device.profile.components[component] + + if compObj then + if component == "WarningDuration" then + local warning_duration = WARNING_DURATION_MAP[mode_set] or DEFAULT_MAX_WARNING_DURATION + device:set_field(ALARM_DURATION, warning_duration, {persist = true}) + end + end + + device.thread:call_with_delay(2,function() + device:emit_component_event( + compObj, + capabilities.mode.mode(mode_set)) + end) end local frient_siren_driver = { NAME = "frient A/S", + lifecycle_handlers = { + added = device_added, + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed, + }, capability_handlers = { [alarm.ID] = { - [alarm.commands.both.NAME] = siren_switch_both_handler, + [alarm.commands.off.NAME] = siren_switch_off_handler, [alarm.commands.siren.NAME] = siren_alarm_siren_handler, - [alarm.commands.strobe.NAME] = siren_alarm_strobe_handler + [alarm.commands.both.NAME] = siren_alarm_siren_handler + }, + [capabilities.tone.ID] = { + [capabilities.tone.commands.beep.NAME] = siren_tone_beep_handler + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = siren_mode_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } }, - [switch.ID] = { - [switch.commands.on.NAME] = siren_switch_on_handler, + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, + [Basic.ID] = { + [DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR] = primary_sw_version_attr_handler + } } }, can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" + return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SIRZB-110" or device:get_model() == "SIRZB-111" or device:get_model() == "SIRZB-112") end } -return frient_siren_driver +return frient_siren_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index 0f08b067fb..21b1865b07 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -32,7 +32,9 @@ local IaswdLevel = IASWD.types.IaswdLevel local capabilities = require "st.capabilities" local alarm = capabilities.alarm local switch = capabilities.switch - +local mode = capabilities.mode +local battery = capabilities.battery +local refresh = capabilities.refresh -- Constants local ALARM_COMMAND = "alarmCommand" local ALARM_LAST_DURATION = "lastDuration" @@ -81,13 +83,13 @@ local send_siren_command = function(device, warning_mode, warning_siren_level, s siren_configuration:set_siren_level(warning_siren_level) device:send( - IASWD.server.commands.StartWarning( - device, - siren_configuration, - data_types.Uint16(warning_duration), - data_types.Uint8(duty_cycle), - data_types.Enum8(strobe_level) - ) + IASWD.server.commands.StartWarning( + device, + siren_configuration, + data_types.Uint16(warning_duration), + data_types.Uint8(duty_cycle), + data_types.Enum8(strobe_level) + ) ) end @@ -157,7 +159,10 @@ end local zigbee_siren_driver_template = { supported_capabilities = { alarm, - switch + switch, + mode, + battery, + refresh }, ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, zigbee_handlers = { @@ -206,4 +211,4 @@ local zigbee_siren_driver_template = { defaults.register_for_default_handlers(zigbee_siren_driver_template, zigbee_siren_driver_template.supported_capabilities) local zigbee_siren = ZigbeeDriver("zigbee-siren", zigbee_siren_driver_template) -zigbee_siren:run() +zigbee_siren:run() \ No newline at end of file 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 098b2ab1bd..f8951d3b31 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -15,110 +15,990 @@ -- Mock out globals local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes +local Basic = clusters.Basic +local Identify = clusters.Identify +local PowerConfiguration = clusters.PowerConfiguration +local Groups = clusters.Groups +local IASZone = clusters.IASZone local IASWD = clusters.IASWD +local IaswdLevel = IASWD.types.IaswdLevel +local WarningMode = IASWD.types.WarningMode +local SquawkMode = IASWD.types.SquawkMode +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local ALARM_DURATION = "warningDuration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local ALARM_DURATION_TEST_VALUE = 5 +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" -local SirenConfiguration = require "st.zigbee.generated.zcl_clusters.IASWD.types.SirenConfiguration" local t_utils = require "integration_test.utils" + local mock_device = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("switch-alarm.yml"), - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "frient A/S", - model = "SIRZB-110", - server_clusters = {0x0502} + { + profile = t_utils.get_profile_definition("frient-siren-battery-source.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-111", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } } - } - } ) 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) +local function set_new_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_older_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010901", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_no_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function get_siren_commands_new_fw( warningMode, sirenLevel, duration ) + local expectedSirenONConfiguration = SirenConfiguration(0x00) + expectedSirenONConfiguration:set_warning_mode(warningMode) --WarningMode.BURGLAR + expectedSirenONConfiguration:set_siren_level(sirenLevel) --IaswdLevel.VERY_HIGH_LEVEL + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_commands_old_fw(warningMode, sirenLevel) + local expectedSirenONConfiguration + local siren_config_value = (sirenLevel << 6) | warningMode + expectedSirenONConfiguration = SirenConfiguration(siren_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_OFF_commands(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_squawk_command_new_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration = SquawkConfiguration(0x00) + expected_squawk_configuration:set_squawk_mode(squawk_mode) + expected_squawk_configuration:set_squawk_level(squawk_siren_level) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +local function get_squawk_command_older_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + expected_squawk_configuration = SquawkConfiguration(squawk_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + ALARM_DEFAULT_MAX_DURATION + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedModes({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedArguments({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.mode("Burglar") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.mode({value = "Very High"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedModes({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedArguments({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.mode("Armed") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.mode("Very High") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedModes( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedArguments( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.mode("4 minutes") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.alarm.alarm.off() + ) + ) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE+1) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_older_firmware_and_defaults() + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_old_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Alarm OFF should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(5) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + + test.mock_time.advance_time(5) + -- stop the siren + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - when maxWarningDuration is shorted then selected mode", + function() + local expectedWarningDuration = 7 + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(expectedWarningDuration, "oneshot") + + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed( + { + preferences = { + maxWarningDuration = expectedWarningDuration + } + } + )) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + expectedWarningDuration + ) + }) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"50 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("50 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL, expectedWarningDuration) + + test.mock_time.advance_time(expectedWarningDuration) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands(expectedWarningDuration) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_older_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of OLD FW version was reported before", + function() + set_older_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "Refresh should be handled - new FW", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Refresh should be handled - FW not known", + 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, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ) + } + ) + test.wait_for_events() + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + test.register_message_test( - "Capability(switch) command(on) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", command = "on", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Power source / battery and tamper clear should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + } + } ) test.register_message_test( - "Capability(alarm) command(both) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "both", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0xC8) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } ) test.register_message_test( - "Capability(alarm) command(siren) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "siren", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0x64) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } ) test.register_message_test( - "Capability(alarm) command(strobe) on should be handled", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "alarm", component = "main", command = "strobe", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, IASWD.server.commands.StartWarning(mock_device, - SirenConfiguration(0xC1), - data_types.Uint16(0x00B4), - data_types.Uint8(00), - data_types.Enum8(00)) } - } - } -) - -test.run_registered_tests() + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.run_registered_tests() \ No newline at end of file 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 new file mode 100644 index 0000000000..810e691849 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua @@ -0,0 +1,1029 @@ +-- Copyright 2025 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 cluster_base = require "st.zigbee.cluster_base" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local OnOff = clusters.OnOff +local Scenes = clusters.Scenes +local Basic = clusters.Basic +local Identify = clusters.Identify +local PowerConfiguration = clusters.PowerConfiguration +local Groups = clusters.Groups +local IASZone = clusters.IASZone +local IASWD = clusters.IASWD +local IaswdLevel = IASWD.types.IaswdLevel +local WarningMode = IASWD.types.WarningMode +local SquawkMode = IASWD.types.SquawkMode +local SirenConfiguration = IASWD.types.SirenConfiguration +local SquawkConfiguration = IASWD.types.SquawkConfiguration +local ZoneStatusAttribute = IASZone.attributes.ZoneStatus + +local PRIMARY_SW_VERSION = "primary_sw_version" +local SIREN_ENDIAN = "siren_endian" +local ALARM_DURATION = "warningDuration" +local ALARM_DEFAULT_MAX_DURATION = 0x00F0 +local ALARM_DURATION_TEST_VALUE = 5 +local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 +local DEVELCO_MANUFACTURER_CODE = 0x1015 + +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local t_utils = require "integration_test.utils" + + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-siren-battery-source-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-111", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } + } +) + +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) + +local function set_new_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_older_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, "010901", {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function set_no_firmware_and_defaults() + -- set the firmware version and endian format for testing + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field(SIREN_ENDIAN, nil, {persist = true}) + -- set test durations and parameters + mock_device:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) +end + +local function get_siren_commands_new_fw(warningMode, sirenLevel, duration) + local expectedSirenONConfiguration = SirenConfiguration(0x00) + expectedSirenONConfiguration:set_warning_mode(warningMode) --WarningMode.BURGLAR + expectedSirenONConfiguration:set_siren_level(sirenLevel) --IaswdLevel.VERY_HIGH_LEVEL + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(duration), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_commands_old_fw(warningMode, sirenLevel) + local expectedSirenONConfiguration + local siren_config_value = (sirenLevel << 6) | warningMode + expectedSirenONConfiguration = SirenConfiguration(siren_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenONConfiguration, + data_types.Uint16(ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_siren_OFF_commands(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.StartWarning( + mock_device, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +local function get_squawk_command_new_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration = SquawkConfiguration(0x00) + expected_squawk_configuration:set_squawk_mode(squawk_mode) + expected_squawk_configuration:set_squawk_level(squawk_siren_level) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +local function get_squawk_command_older_fw(squawk_mode, squawk_siren_level) + local expected_squawk_configuration + local squawk_config_value = (squawk_siren_level << 6) | squawk_mode + expected_squawk_configuration = SquawkConfiguration(squawk_config_value) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.server.commands.Squawk( + mock_device, + expected_squawk_configuration + ) + }) +end + +test.register_coroutine_test( + "lifecycles - init and doConfigure test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + ALARM_DEFAULT_MAX_DURATION + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, + 30, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0, + 21600, + 1 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + end +) + +test.register_coroutine_test( + "lifecycle - added test", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedModes({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.supportedArguments({ "Burglar", "Fire", "Emergency", "Panic", "Panic Fire", "Panic Emergency" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVoice", + capabilities.mode.mode("Burglar") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SirenVolume", + capabilities.mode.mode({value = "Very High"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedModes({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.supportedArguments({ "Armed", "Disarmed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVoice", + capabilities.mode.mode("Armed") + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedModes({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.supportedArguments({ "Low", "Medium", "High", "Very High" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "SquawkVolume", + capabilities.mode.mode("Very High") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedModes( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.supportedArguments( + { + "5 seconds", "10 seconds", "15 seconds", "20 seconds", "25 seconds", "30 seconds", "40 seconds", "50 seconds", + "1 minute", "2 minutes", "3 minutes", "4 minutes", "5 minutes", "10 minutes" + }, + { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "WarningDuration", + capabilities.mode.mode("4 minutes") + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.alarm.alarm.off() + ) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.tamperAlert.tamper.clear() + ) + ) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on the siren (test with default settings)", + function() + set_older_firmware_and_defaults() + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_old_fw(WarningMode.BURGLAR,IaswdLevel.VERY_HIGH_LEVEL) + test.mock_time.advance_time(ALARM_DURATION_TEST_VALUE) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Alarm OFF should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + get_siren_OFF_commands() + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + test.mock_time.advance_time(5) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"5 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("5 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL,ALARM_DURATION_TEST_VALUE) + + test.mock_time.advance_time(5) + -- stop the siren + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the OFF command + get_siren_OFF_commands() + end +) + +test.register_coroutine_test( + "SirenVoice mode 'Fire' and SirenVolume mode 'LOW' should be handled - when maxWarningDuration is shorted then selected mode", + function() + local expectedWarningDuration = 7 + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(expectedWarningDuration, "oneshot") + + test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed( + { + preferences = { + maxWarningDuration = expectedWarningDuration + } + } + )) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write( + mock_device, + expectedWarningDuration + ) + }) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVoice", command = "setMode", args = {"Fire"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVoice", capabilities.mode.mode("Fire")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SirenVolume", command = "setMode", args = {"Low"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SirenVolume", capabilities.mode.mode("Low")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "WarningDuration", command = "setMode", args = {"50 seconds"} } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("WarningDuration", capabilities.mode.mode("50 seconds")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "alarm", component = "main", command = "siren", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_siren_commands_new_fw(WarningMode.FIRE,IaswdLevel.LOW_LEVEL, expectedWarningDuration) + + test.mock_time.advance_time(expectedWarningDuration) + -- stop the siren + -- Expect the OFF command + get_siren_OFF_commands(expectedWarningDuration) + end +) + +test.register_coroutine_test( + "Should detect newer firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_new_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) >= "010903", "PRIMARY_SW_VERSION should be greater than or equal to '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Should detect older firmware version and use correct endian format to turn on squawk (test with default settings)", + function() + set_older_firmware_and_defaults() + + -- Verify fields are set correctly + assert(mock_device:get_field(PRIMARY_SW_VERSION) < "010903", "PRIMARY_SW_VERSION should be lower than '010903'") + + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Test the siren command with reversed endian + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw( SquawkMode.SOUND_FOR_SYSTEM_IS_ARMED, IaswdLevel.VERY_HIGH_LEVEL ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled", + function() + set_new_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of OLD FW version was reported before", + function() + set_older_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Expect the command with given configuration + get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "SquawkVoice mode 'Disarmed' and SquawkVolume mode 'Medium' should be handled - in case of NO FW version was reported before", + function() + set_no_firmware_and_defaults() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__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") + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVoice", command = "setMode", args = { "Disarmed" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVoice", capabilities.mode.mode("Disarmed")) + ) + + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "SquawkVolume", command = "setMode", args = { "Medium" } } + }) + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("SquawkVolume", capabilities.mode.mode("Medium")) + ) + + test.wait_for_events() + -- Test siren with update configuration + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "tone", component = "main", command = "beep", args = {} } + }) + test.mock_time.advance_time(1) + -- Read the FW version first + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ):to_endpoint(0x2B) + } + ) + -- Expect the command with given configuration + get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) + end +) + +test.register_coroutine_test( + "Refresh should be handled - new FW", + function() + set_new_firmware_and_defaults() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + }) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Refresh should be handled - FW not known", + 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, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Basic.ID, + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + DEVELCO_MANUFACTURER_CODE + ) + } + ) + + test.wait_for_events() + end +) + +test.register_message_test( + "Power source / mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0005) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Power source / battery should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Min battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0xC8) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_message_test( + "Medium battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0x64) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + +test.register_message_test( + "Max battery voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 0) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua index e8de7eb958..3dc707a297 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua @@ -19,6 +19,7 @@ local zcl_cmds = require "st.zigbee.zcl.global_commands" local IASZone = clusters.IASZone local IASWD = clusters.IASWD local OnOff = clusters.OnOff +local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" @@ -212,6 +213,20 @@ test.register_coroutine_test( mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + 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, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end @@ -265,6 +280,14 @@ test.register_message_test( OnOff.attributes.OnOff:read(mock_device) } }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + } + } }, { inner_block_ordering = "relaxed" From a963bb5e9a4358cd5d4444924048ccc562faba5e Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 5 Feb 2026 10:38:51 -0800 Subject: [PATCH 406/449] WWSTCERT-10212 Osram SMART WIFI MATTER WALL SWITCH 1G --- 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 e418d34d3b..948279c3f3 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -1837,6 +1837,11 @@ matterManufacturer: vendorId: 0x1189 productId: 0x0B93 deviceProfileName: light-color-level + - id: "4489/2194" + deviceLabel: SMART WIFI MATTER WALL SWITCH 1G + vendorId: 0x1189 + productId: 0x0892 + deviceProfileName: switch-binary #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 6f4e5d2730ec27799d3966f8842cbcb42e409fab Mon Sep 17 00:00:00 2001 From: Marcin Krystian Tyminski <81477027+marcintyminski@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:49:54 +0100 Subject: [PATCH 407/449] Merge pull request #2454 from marcintyminski/Add-support-to-frient-vibration-sensor-WISZB-137 WWSTCERT-8745 Add support to frient vibration sensor wiszb 137 --- .../zigbee-contact/fingerprints.yml | 5 + ...cceleration-motion-temperature-battery.yml | 50 ++ ...ion-motion-temperature-contact-battery.yml | 89 +++ .../zigbee-contact/src/configurations.lua | 16 + .../src/frient/frient-vibration/init.lua | 225 ++++++++ .../zigbee-contact/src/frient/init.lua | 5 +- .../src/test/test_frient_vibration_sensor.lua | 510 ++++++++++++++++++ 7 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml create mode 100644 drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml create mode 100644 drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index f35ca2f7ba..c30c3296a2 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -154,6 +154,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: WISZB-131 deviceProfileName: frient-contact-battery-temperature + - id: "frient A/S/WISZB-137" + deviceLabel: frient Vibration Sensor + manufacturer: frient A/S + model: WISZB-137 + deviceProfileName: acceleration-motion-temperature-battery - id: "Compacta/ZBWDS" deviceLabel: Smartenit Open/Closed Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml new file mode 100644 index 0000000000..4f90f8a11e --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -0,0 +1,50 @@ +name: acceleration-motion-temperature-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 + - title: "Use with Contact Sensor" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "No" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml new file mode 100644 index 0000000000..c40eb49b9e --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -0,0 +1,89 @@ +name: acceleration-motion-temperature-contact-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: contactSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 + - title: "Use with Contact Sensor" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "Yes" + - title: "Axis to activate Contact Sensor" + name: contactSensorAxis + required: false + preferenceType: enumeration + definition: + options: + "X": "X" + "Y": "Y" + "Z": "Z" + default: "Z" + - title: "Initial position (closed state)" + name: sensorInitialPosition + description: "Initial position of the device in the chosen axis" + required: false + preferenceType: number + definition: + minimum: -2000 + maximum: 2000 + default: 0 + - title: "Contact Sensor threshold (open)" + name: contactSensorValue + description: "Value change required to trigger contact sensor" + required: false + preferenceType: number + definition: + minimum: 20 + maximum: 4000 + default: 900 + - title: "Measurement tolerance" + name: tolerance + description: "Set the tolerance in percentage of the threshold" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 20 + default: 0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index aa28e1e43f..1e08ecffd4 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -129,6 +129,22 @@ local devices = { } } }, + FRIENT_VIBRATION_SENSOR_WISZB_137 = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137" } + }, + CONFIGURATION = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 3600, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1, + endpoint = 0x2D + } + } + } } local configurations = {} diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua new file mode 100644 index 0000000000..2514bfb63b --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -0,0 +1,225 @@ +-- Copyright 2025 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 zcl_commands = require "st.zigbee.zcl.global_commands" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local device_management = require "st.zigbee.device_management" +local data_types = require "st.zigbee.data_types" +local threeAxis = capabilities.threeAxis + +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local IASZone = zcl_clusters.IASZone +local PowerConfiguration = zcl_clusters.PowerConfiguration +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137", }, +} + +local function can_handle_vibration_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.Int16 }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.Int16 }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.Int16 } + }, +} + +local function acceleration_measure_report_handler(driver, device, zb_rx) + local measured_x, measured_y, measured_z + + for _, attribute_record in ipairs(zb_rx.body.zcl_body.attr_records) do + local attribute_id = attribute_record.attr_id.value + local axis_value = attribute_record.data.value + + if attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID then + measured_x = axis_value + elseif attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID then + measured_y = axis_value + elseif attribute_id == Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID then + measured_z = axis_value + end + end + + if measured_x and measured_y and measured_z then + device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) + + if device:supports_capability(capabilities.contactSensor) then + local garageAxis = measured_x + if device.preferences.contactSensorAxis == "Y" then + garageAxis = measured_y + elseif device.preferences.contactSensorAxis == "Z" then + garageAxis = measured_z + end + local initial_position = device.preferences.sensorInitialPosition or 0 + if math.abs(initial_position - garageAxis) >= device.preferences.contactSensorValue - device.preferences.contactSensorValue * (device.preferences.tolerance / 100) then + device:emit_event(capabilities.contactSensor.contact.open()) + else + device:emit_event(capabilities.contactSensor.contact.closed()) + end + end + end +end + +local function get_cluster_configurations() + return { + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + } + } +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + device:emit_event(zone_status:is_alarm1_set() and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event(zone_status:is_alarm2_set() and capabilities.accelerationSensor.acceleration.active() or capabilities.accelerationSensor.acceleration.inactive()) +end + +local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx) + generate_event_from_zone_status(driver, device, attr_val, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + +local function device_init(driver, device) + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + --Add the manufacturer-specific attributes to generate their configure reporting and bind requests + for _, config in pairs(get_cluster_configurations()) do + device:add_configured_attribute(config) + end +end + +local function do_refresh(driver, device) + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(TEMPERATURE_ENDPOINT)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) +end + +local function do_configure(driver, device, event, args) + device:configure() + + device:send(device_management.build_bind_request(device, zcl_clusters.IASZone.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(IASZone.attributes.ZoneStatus:configure_reporting(device, 0, 1*60*60, 1):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(device_management.build_bind_request(device, Frient_AccelerationMeasurementCluster.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1) * 100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1 * 60 * 60, sensitivity):to_endpoint(TEMPERATURE_ENDPOINT)) + + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + if args and args.old_st_store then + if args.old_st_store.preferences.sensitivityLevel ~= device.preferences.sensitivityLevel then + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(0x2D)) + end + if args.old_st_store.preferences.temperatureSensitivity ~= device.preferences.temperatureSensitivity then + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1)*100 + 0.5) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1*60*60, sensitivity):to_endpoint(0x26)) + end + if args.old_st_store.preferences.garageSensor ~= device.preferences.garageSensor then + if device.preferences.garageSensor == "Yes" then + device:try_update_metadata({profile = "acceleration-motion-temperature-contact-battery"}) + elseif device.preferences.garageSensor == "No" then + device:try_update_metadata({profile = "acceleration-motion-temperature-battery"}) + end + end + device.thread:call_with_delay(5, function() + device:refresh() + end) + end +end + +local frient_vibration_driver_template = { + NAME = "frient vibration driver", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed + }, + zigbee_handlers = { + global = { + [Frient_AccelerationMeasurementCluster.ID] = { + [zcl_commands.ReportAttribute.ID] = acceleration_measure_report_handler, + [zcl_commands.ReadAttributeResponse.ID] = acceleration_measure_report_handler + } + }, + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = can_handle_vibration_sensor +} + +return frient_vibration_driver_template \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index e732664d91..1379d0a8c0 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -80,6 +80,9 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, + sub_drivers = { + require("frient/frient-vibration"), + }, zigbee_handlers = { cluster = { [IASZone.ID] = { @@ -93,7 +96,7 @@ local frient_sensor = { } }, can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S" and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131")) + return (device:get_manufacturer() == "frient A/S") and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131" or device:get_model() == "WISZB-137") end } 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 new file mode 100644 index 0000000000..2e704a61ca --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -0,0 +1,510 @@ + +-- Copyright 2025 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 test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local base64 = require "base64" +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + +local mock_device_contact = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-contact-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.name_to_id_map["Int16"] } + }, +} + +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_contact) +end + +test.set_test_init_function(test_init) + +local function custom_configure_reporting(device, cluster, attribute, data_type, min_interval, max_interval, reportable_change, mfg_code) + local message = cluster_base.configure_reporting(device, + data_types.ClusterId(cluster), + data_types.AttributeId(attribute), + data_type, + min_interval, + max_interval, + reportable_change) + + -- Set the manufacturer-specific bit and add the manufacturer code + message.body.zcl_header.frame_ctrl:set_mfg_specific() + message.body.zcl_header.mfg_code = data_types.validate_or_build_type(mfg_code, data_types.Uint16, "mfg_code") + + return message +end + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "init" }) + + test.wait_for_events() + + --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device_contact, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_ENDPOINT + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0x001E, + 0x012C, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 30, + 600, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.IASCIEAddress:write( + mock_device_contact, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + custom_configure_reporting( + mock_device_contact, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device_contact, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.CurrentZoneSensitivityLevel:write( + mock_device_contact, + 0x000A + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 0x001E, + 0x0E10, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0, + 3600, + 0 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + mock_device_contact:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature measurement cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2300)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 23.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { "register_native_capability_attr_handler", + { + device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" + } + } + } + } +) + +test.register_message_test( + "Battery min voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Battery max voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_coroutine_test( +"Refresh necessary 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, IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_ENDPOINT) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) +end +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: active motion and inactive acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive(mock_device)) + } + } +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: inactive motion and active acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0002)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active(mock_device)) + } + } +) + +test.register_coroutine_test( + "Three Axis report should be correctly handled", + function() + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + end +) + +test.register_coroutine_test( + "Contact sensor open events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, -902}, + } + test.socket.zigbee:__queue_receive({ + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + end +) + +test.register_coroutine_test( + "Contact sensor close events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.closed()) + ) + end +) + +test.run_registered_tests() \ No newline at end of file From 828187bb9bf68984ace6ee1d25bcc39b4557dfc5 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 5 Feb 2026 11:02:54 -0800 Subject: [PATCH 408/449] WWSTCERT-10019 WINDOWSTORY GATEWAY-MT --- drivers/SmartThings/matter-window-covering/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-window-covering/fingerprints.yml b/drivers/SmartThings/matter-window-covering/fingerprints.yml index 0de1e91af8..531dc8c36f 100644 --- a/drivers/SmartThings/matter-window-covering/fingerprints.yml +++ b/drivers/SmartThings/matter-window-covering/fingerprints.yml @@ -107,6 +107,12 @@ matterManufacturer: vendorId: 0x139C productId: 0xFA17 deviceProfileName: window-covering +#WINDOWSTORY + - id: "5496/6657" + deviceLabel: GATEWAY-MT + vendorId: 0x1578 + productId: 0x1A01 + deviceProfileName: window-covering-tilt #WISTAR - id: "5207/3" deviceLabel: WISTAR WSERD16-B Smart Tubular Motor From 454b00ff016c9427beadb5ea6d733b27a69677ba Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 15 Dec 2025 15:27:02 -0600 Subject: [PATCH 409/449] CHAD-17156: Lazy loading of matter-lock sub-drivers --- .../matter-lock/src/DoorLock/init.lua | 6 +- .../matter-lock/src/DoorLock/types/init.lua | 5 +- drivers/SmartThings/matter-lock/src/init.lua | 20 ++---- .../matter-lock/src/lazy_load_subdriver.lua | 14 +++++ .../matter-lock/src/lock_utils.lua | 15 +---- .../src/new-matter-lock/can_handle.lua | 19 ++++++ .../src/new-matter-lock/fingerprints.lua | 35 +++++++++++ .../matter-lock/src/new-matter-lock/init.lua | 63 ++----------------- .../matter-lock/src/sub_drivers.lua | 8 +++ .../src/test/test_aqara_matter_lock.lua | 16 +---- .../src/test/test_bridged_matter_lock.lua | 16 +---- .../matter-lock/src/test/test_matter_lock.lua | 16 +---- .../src/test/test_matter_lock_battery.lua | 16 +---- .../test/test_matter_lock_batteryLevel.lua | 16 +---- .../src/test/test_matter_lock_codes.lua | 16 +---- .../src/test/test_matter_lock_cota.lua | 16 +---- .../src/test/test_matter_lock_modular.lua | 16 +---- .../src/test/test_matter_lock_unlatch.lua | 16 +---- .../src/test/test_new_matter_lock.lua | 16 +---- .../src/test/test_new_matter_lock_battery.lua | 16 +---- 20 files changed, 129 insertions(+), 232 deletions(-) create mode 100644 drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua create mode 100644 drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua create mode 100644 drivers/SmartThings/matter-lock/src/sub_drivers.lua diff --git a/drivers/SmartThings/matter-lock/src/DoorLock/init.lua b/drivers/SmartThings/matter-lock/src/DoorLock/init.lua index b9675ebf77..20cf12d0ea 100644 --- a/drivers/SmartThings/matter-lock/src/DoorLock/init.lua +++ b/drivers/SmartThings/matter-lock/src/DoorLock/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local cluster_base = require "st.matter.cluster_base" local DoorLockServerAttributes = require "DoorLock.server.attributes" local DoorLockServerCommands = require "DoorLock.server.commands" @@ -255,4 +259,4 @@ setmetatable(DoorLock.events, event_helper_mt) setmetatable(DoorLock, {__index = cluster_base}) -return DoorLock \ No newline at end of file +return DoorLock diff --git a/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua b/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua index 461d914f84..f4b2939d61 100644 --- a/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua +++ b/drivers/SmartThings/matter-lock/src/DoorLock/types/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local types_mt = {} types_mt.__types_cache = {} types_mt.__index = function(self, key) @@ -11,4 +14,4 @@ local DoorLockTypes = {} setmetatable(DoorLockTypes, types_mt) -return DoorLockTypes \ No newline at end of file +return DoorLockTypes diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 6133b74e45..b3403863ec 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local MatterDriver = require "st.matter.driver" local clusters = require "st.matter.clusters" @@ -717,9 +707,7 @@ local matter_lock_driver = { capabilities.battery, capabilities.batteryLevel, }, - sub_drivers = { - require("new-matter-lock"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, added = device_added, diff --git a/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..a04740d267 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + local MatterDriver = require "st.matter.driver" + local version = require "version" + if version.api >= 16 then + return MatterDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return MatterDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index 816ca446f2..94e95c196f 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local lock_utils = { -- Lock device field names 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 new file mode 100644 index 0000000000..7bbb45ed44 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua @@ -0,0 +1,19 @@ +-- 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 new file mode 100644 index 0000000000..bddc7b1fd1 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -0,0 +1,35 @@ +-- 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 + {0x147F, 0x0001}, -- U-tec + {0x147F, 0x0008}, -- Ultraloq, Bolt 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 + {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, 0x0042}, -- Kwikset Halo Select Plus + {0x1421, 0x0081}, -- Kwikset Aura Reach +} + +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 index e47a883143..11aa2b0884 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -1,18 +1,7 @@ --- Copyright 2024 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 device_lib = require "st.device" +-- 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" @@ -60,36 +49,6 @@ local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY } -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 - {0x147F, 0x0001}, -- U-tec - {0x147F, 0x0008}, -- Ultraloq, Bolt 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 - {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, 0x0042}, -- Kwikset Halo Select Plus - {0x1421, 0x0081}, -- Kwikset Aura Reach -} local battery_support = { NO_BATTERY = "NO_BATTERY", @@ -152,18 +111,6 @@ local subscribed_events = { } } -local function is_new_matter_lock_products(opts, driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - for _, p in ipairs(NEW_MATTER_LOCK_PRODUCTS) do - if device.manufacturer_info.vendor_id == p[1] and - device.manufacturer_info.product_id == p[2] then - return true - end - end - return false -end local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -2971,7 +2918,7 @@ local new_matter_lock_handler = { capabilities.battery, capabilities.batteryLevel }, - can_handle = is_new_matter_lock_products + 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 new file mode 100644 index 0000000000..0f5d1f76d2 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("new-matter-lock"), +} +return sub_drivers 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 5c4add0ff5..c5da1b600e 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) 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 0d17cafda6..eb8ec9fa98 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" 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 0db34172d5..8fd0cc0574 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 8e0be6359d..a8f7506798 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" 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 c53547025c..990bff8b60 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 2960d905c2..a3e05c9634 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 57468ef2fc..c41d911881 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 d6382faa06..c4c226fbbe 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 38154d7b04..642ca3bf7a 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) 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 7d5a59ff32..738248fd8e 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,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) 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 6926f1d62d..03a51a3150 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" test.set_rpc_version(0) From 05bef658db425512d6029b6a921d05ef3406163f Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:33 -0600 Subject: [PATCH 410/449] 17074: zigbee-siren lazy load subdrivers --- .../zigbee-siren/src/frient/can_handle.lua | 15 +++++++++++++ .../zigbee-siren/src/frient/init.lua | 21 +++++-------------- drivers/SmartThings/zigbee-siren/src/init.lua | 18 ++++------------ .../zigbee-siren/src/lazy_load_subdriver.lua | 15 +++++++++++++ .../zigbee-siren/src/ozom/can_handle.lua | 11 ++++++++++ .../zigbee-siren/src/ozom/init.lua | 20 ++++-------------- .../zigbee-siren/src/sub_drivers.lua | 9 ++++++++ .../src/test/test_frient_siren.lua | 2 +- .../zigbee-siren/src/test/test_ozom_siren.lua | 2 +- 9 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-siren/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua new file mode 100644 index 0000000000..ed6acf5b42 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and + (device:get_model() == "SIRZB-110" or + device:get_model() == "SIRZB-111" or + device:get_model() == "SIRZB-112") + then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-siren/src/frient/init.lua b/drivers/SmartThings/zigbee-siren/src/frient/init.lua index 78738ca9a8..d408579696 100644 --- a/drivers/SmartThings/zigbee-siren/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/frient/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local data_types = require "st.zigbee.data_types" local battery_defaults = require "st.zigbee.defaults.battery_defaults" --ZCL @@ -419,9 +410,7 @@ local frient_siren_driver = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SIRZB-110" or device:get_model() == "SIRZB-111" or device:get_model() == "SIRZB-112") - end + can_handle = require("frient.can_handle"), } return frient_siren_driver \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/init.lua b/drivers/SmartThings/zigbee-siren/src/init.lua index 21b1865b07..b1ad81c9c6 100644 --- a/drivers/SmartThings/zigbee-siren/src/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -194,7 +184,7 @@ local zigbee_siren_driver_template = { added = device_added, doConfigure = do_configure }, - sub_drivers = { require("ozom"), require("frient") }, + sub_drivers = require("sub_drivers"), cluster_configurations = { [alarm.ID] = { { diff --git a/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua b/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua new file mode 100644 index 0000000000..bec6069a7f --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/ozom/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ozom_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "ClimaxTechnology" then + return true, require("ozom") + end + return false +end + +return ozom_can_handle diff --git a/drivers/SmartThings/zigbee-siren/src/ozom/init.lua b/drivers/SmartThings/zigbee-siren/src/ozom/init.lua index e4cecc28ab..c17a1c4fb7 100644 --- a/drivers/SmartThings/zigbee-siren/src/ozom/init.lua +++ b/drivers/SmartThings/zigbee-siren/src/ozom/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" --ZCL @@ -75,9 +65,7 @@ local ozom_siren_driver = { [switch.commands.on.NAME] = siren_switch_on_handler } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ClimaxTechnology" - end + can_handle = require("ozom.can_handle"), } return ozom_siren_driver diff --git a/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua new file mode 100644 index 0000000000..05d186b87e --- /dev/null +++ b/drivers/SmartThings/zigbee-siren/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("frient"), + lazy_load_if_possible("ozom"), +} 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 f8951d3b31..8140da64bd 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. 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 5b9a6a2bea..f05c58468f 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua @@ -1,4 +1,4 @@ --- Copyright 2022 SmartThings +-- Copyright 2022 SmartThings, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. From 5ab66d3d34b5a3f54ed963250f31d6147334870c Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:05 -0600 Subject: [PATCH 411/449] CHAD-17065: zigbee-contact lazy loading subdrivers --- .../zigbee-contact/src/aqara/can_handle.lua | 14 ++++++ .../zigbee-contact/src/aqara/fingerprints.lua | 8 +++ .../zigbee-contact/src/aqara/init.lua | 16 ++---- .../src/aurora-contact-sensor/can_handle.lua | 14 ++++++ .../aurora-contact-sensor/fingerprints.lua | 9 ++++ .../src/aurora-contact-sensor/init.lua | 30 ++--------- .../zigbee-contact/src/configurations.lua | 15 +----- .../contact-temperature-sensor/can_handle.lua | 14 ++++++ .../ecolink-contact/can_handle.lua | 14 ++++++ .../ecolink-contact/fingerprints.lua | 9 ++++ .../ecolink-contact/init.lua | 30 ++--------- .../fingerprints.lua | 24 +++++++++ .../src/contact-temperature-sensor/init.lua | 50 +++---------------- .../sub_drivers.lua | 8 +++ .../zigbee-contact/src/frient/can_handle.lua | 15 ++++++ .../frient/frient-vibration/can_handle.lua | 15 ++++++ .../src/frient/frient-vibration/init.lua | 17 +------ .../zigbee-contact/src/frient/init.lua | 24 ++------- .../zigbee-contact/src/frient/sub_drivers.lua | 8 +++ .../SmartThings/zigbee-contact/src/init.lua | 26 ++-------- .../src/lazy_load_subdriver.lua | 15 ++++++ .../src/multi-sensor/can_handle.lua | 14 ++++++ .../centralite-multi/can_handle.lua | 11 ++++ .../multi-sensor/centralite-multi/init.lua | 20 ++------ .../src/multi-sensor/fingerprints.lua | 13 +++++ .../zigbee-contact/src/multi-sensor/init.lua | 42 +++------------- .../src/multi-sensor/multi_utils.lua | 17 ++----- .../multi-sensor/samjin-multi/can_handle.lua | 11 ++++ .../src/multi-sensor/samjin-multi/init.lua | 20 ++------ .../smartthings-multi/can_handle.lua | 11 ++++ .../multi-sensor/smartthings-multi/init.lua | 20 ++------ .../src/multi-sensor/sub_drivers.lua | 11 ++++ .../thirdreality-multi/can_handle.lua | 11 ++++ .../multi-sensor/thirdreality-multi/init.lua | 20 ++------ .../zigbee-contact/src/sengled/can_handle.lua | 14 ++++++ .../src/sengled/fingerprints.lua | 8 +++ .../zigbee-contact/src/sengled/init.lua | 16 ++---- .../src/smartsense-multi/can_handle.lua | 17 +++++++ .../src/smartsense-multi/fingerprints.lua | 9 ++++ .../src/smartsense-multi/init.lua | 34 ++----------- .../zigbee-contact/src/sub_drivers.lua | 14 ++++++ .../src/test/test_aqara_contact_sensor.lua | 15 +----- .../src/test/test_aurora_contact_sensor.lua | 15 +----- .../src/test/test_centralite_multi_sensor.lua | 15 +----- .../test/test_contact_temperature_sensor.lua | 15 +----- .../src/test/test_ecolink_contact.lua | 15 +----- .../src/test/test_ewelink_heiman_sensor.lua | 15 +----- .../src/test/test_frient_contact_sensor.lua | 15 +----- .../test/test_frient_contact_sensor_2_pro.lua | 15 +----- .../test/test_frient_contact_sensor_pro.lua | 15 +----- .../src/test/test_orvibo_contact_sensor.lua | 15 +----- .../src/test/test_samjin_multi_sensor.lua | 15 +----- .../src/test/test_sengled_contact_sensor.lua | 15 +----- .../src/test/test_smartsense_multi.lua | 17 ++----- .../test/test_smartthings_multi_sensor.lua | 15 +----- .../src/test/test_third_reality_contact.lua | 15 +----- .../src/test/test_zigbee_contact.lua | 15 +----- .../src/test/test_zigbee_contact_battery.lua | 15 +----- .../src/test/test_zigbee_contact_tyco.lua | 15 +----- 59 files changed, 411 insertions(+), 569 deletions(-) create mode 100644 drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua new file mode 100644 index 0000000000..7877f2c180 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..bcad0d779d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.magnet.agl02" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua index 477a62afd4..9093ee7bb9 100644 --- a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" @@ -13,9 +16,6 @@ local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local PRIVATE_HEART_BATTERY_ENERGY_ID = 0x00F7 -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.magnet.agl02" } -} local CONFIGURATIONS = { { @@ -36,14 +36,6 @@ local CONFIGURATIONS = { } } -local is_aqara_products = function(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_init(driver, device) device:remove_configured_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID) @@ -136,7 +128,7 @@ local aqara_contact_handler = { doConfigure = do_configure, added = added_handler, }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_contact_handler diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua new file mode 100644 index 0000000000..ceb3f4d933 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aurora_contact(opts, driver, device, ...) + local FINGERPRINTS = require("aurora-contact-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aurora-contact-sensor") + end + end + return false +end + +return can_handle_aurora_contact diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua new file mode 100644 index 0000000000..022bbc83d5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AURORA_CONTACT_FINGERPRINTS = { + { mfr = "Aurora", model = "DoorSensor50AU" }, -- Aurora Smart Door/Window Sensor + { mfr = "Aurora", model = "WindowSensor51AU" } -- Aurora Smart Door/Window Sensor +} + +return AURORA_CONTACT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua index efa6fb8c37..b5ac4fe8bb 100644 --- a/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aurora-contact-sensor/init.lua @@ -1,26 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone -local AURORA_CONTACT_FINGERPRINTS = { - { mfr = "Aurora", model = "DoorSensor50AU" }, -- Aurora Smart Door/Window Sensor - { mfr = "Aurora", model = "WindowSensor51AU" } -- Aurora Smart Door/Window Sensor -} local AURORA_CONTACT_CONFIGURATION = { { @@ -33,14 +19,6 @@ local AURORA_CONTACT_CONFIGURATION = { } } -local function can_handle_aurora_contact(opts, driver, device, ...) - for _, fingerprint in ipairs(AURORA_CONTACT_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_init(driver, device) battery_defaults.use_battery_voltage_handling(device) @@ -55,7 +33,7 @@ local aurora_contact = { lifecycle_handlers = { init = device_init }, - can_handle = can_handle_aurora_contact + can_handle = require("aurora-contact-sensor.can_handle"), } return aurora_contact diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index 1e08ecffd4..cd5ede1b6e 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua new file mode 100644 index 0000000000..2c9f359dbe --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_contact_temperature_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("contact-temperature-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("contact-temperature-sensor") + end + end + return false +end + +return can_handle_contact_temperature_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua new file mode 100644 index 0000000000..b17c1efb21 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ecolink_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("contact-temperature-sensor.ecolink-contact.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("contact-temperature-sensor.ecolink-contact") + end + end + return false +end + +return can_handle_ecolink_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua new file mode 100644 index 0000000000..c86e6e073f --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS = { + { mfr = "Ecolink", model = "4655BC0-R" }, + { mfr = "Ecolink", model = "DWZB1-ECO" } +} + +return ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua index 22999c59aa..31dc67fd3e 100644 --- a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/ecolink-contact/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" @@ -22,19 +12,7 @@ local SHORT_POLL_INTERVAL = 0x0200 local LONG_POLL_INTERVAL = 0xB1040000 local FAST_POLL_TIMEOUT = 0x0028 -local ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS = { - { mfr = "Ecolink", model = "4655BC0-R" }, - { mfr = "Ecolink", model = "DWZB1-ECO" } -} -local function can_handle_ecolink_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ECOLINK_CONTACT_TEMPERATURE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_configure(driver, device) device:configure() @@ -51,7 +29,7 @@ local ecolink_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_ecolink_sensor + can_handle = require("contact-temperature-sensor.ecolink-contact.can_handle"), } return ecolink_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua new file mode 100644 index 0000000000..4efc09684b --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/fingerprints.lua @@ -0,0 +1,24 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3300-S" }, + { mfr = "CentraLite", model = "3300" }, + { mfr = "CentraLite", model = "3320-L" }, + { mfr = "CentraLite", model = "3323-G" }, + { mfr = "CentraLite", model = "Contact Sensor-A" }, + { mfr = "Visonic", model = "MCT-340 E" }, + { mfr = "Visonic", model = "MCT-340 SMA" }, + { mfr = "Ecolink", model = "4655BC0-R" }, + { mfr = "Ecolink", model = "DWZB1-ECO" }, + { mfr = "iMagic by GreatStar", model = "1116-S" }, + { mfr = "Bosch", model = "RFMS-ZBMS" }, + { mfr = "Megaman", model = "MS601/z1" }, + { mfr = "AduroSmart Eria", model = "CSW_ADUROLIGHT" }, + { mfr = "ADUROLIGHT", model = "CSW_ADUROLIGHT" }, + { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, + { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, + { mfr = "Compacta", model = "ZBWDS" } +} + +return CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua index 33b2e3daa3..76f822fedd 100644 --- a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/init.lua @@ -1,48 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local configurationMap = require "configurations" -local CONTACT_TEMPERATURE_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3300-S" }, - { mfr = "CentraLite", model = "3300" }, - { mfr = "CentraLite", model = "3320-L" }, - { mfr = "CentraLite", model = "3323-G" }, - { mfr = "CentraLite", model = "Contact Sensor-A" }, - { mfr = "Visonic", model = "MCT-340 E" }, - { mfr = "Visonic", model = "MCT-340 SMA" }, - { mfr = "Ecolink", model = "4655BC0-R" }, - { mfr = "Ecolink", model = "DWZB1-ECO" }, - { mfr = "iMagic by GreatStar", model = "1116-S" }, - { mfr = "Bosch", model = "RFMS-ZBMS" }, - { mfr = "Megaman", model = "MS601/z1" }, - { mfr = "AduroSmart Eria", model = "CSW_ADUROLIGHT" }, - { mfr = "ADUROLIGHT", model = "CSW_ADUROLIGHT" }, - { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, - { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, - { mfr = "Compacta", model = "ZBWDS" } -} -local function can_handle_contact_temperature_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(CONTACT_TEMPERATURE_SENSOR_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_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -61,10 +25,8 @@ local contact_temperature_sensor = { lifecycle_handlers = { init = device_init }, - sub_drivers = { - require("contact-temperature-sensor/ecolink-contact") - }, - can_handle = can_handle_contact_temperature_sensor + sub_drivers = require("contact-temperature-sensor.sub_drivers"), + can_handle = require("contact-temperature-sensor.can_handle"), } return contact_temperature_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua new file mode 100644 index 0000000000..d47c4ad123 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/contact-temperature-sensor/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("contact-temperature-sensor.ecolink-contact"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua new file mode 100644 index 0000000000..e2d56c3dc0 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if (device:get_manufacturer() == "frient A/S") and + (device:get_model() == "WISZB-120" or + device:get_model() == "WISZB-121" or + device:get_model() == "WISZB-131" or + device:get_model() == "WISZB-137") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua new file mode 100644 index 0000000000..14ecae440a --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137", }, +} + +return function(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("frient.frient-vibration") + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index 2514bfb63b..bba3dc0bdf 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -27,19 +27,6 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D local TEMPERATURE_ENDPOINT = 0x26 -local FRIENT_DEVICE_FINGERPRINTS = { - { mfr = "frient A/S", model = "WISZB-137", }, -} - -local function can_handle_vibration_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local Frient_AccelerationMeasurementCluster = { ID = 0xFC04, ManufacturerSpecificCode = 0x1015, @@ -219,7 +206,7 @@ local frient_vibration_driver_template = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = can_handle_vibration_sensor + can_handle = require ("frient.frient-vibration.can_handle") } -return frient_vibration_driver_template \ No newline at end of file +return frient_vibration_driver_template diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index 1379d0a8c0..f28cad8cdb 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -80,9 +70,7 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, - sub_drivers = { - require("frient/frient-vibration"), - }, + sub_drivers = require("frient.sub_drivers"), zigbee_handlers = { cluster = { [IASZone.ID] = { @@ -95,9 +83,7 @@ local frient_sensor = { } } }, - can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S") and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131" or device:get_model() == "WISZB-137") - end + can_handle = require("frient.can_handle"), } return frient_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua new file mode 100644 index 0000000000..cfaca8fca3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("frient.frient-vibration"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/init.lua b/drivers/SmartThings/zigbee-contact/src/init.lua index 67be42e671..63a7bf7565 100644 --- a/drivers/SmartThings/zigbee-contact/src/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -97,15 +87,7 @@ local zigbee_contact_driver_template = { init = device_init, added = added_handler, }, - sub_drivers = { - require("aqara"), - require("aurora-contact-sensor"), - require("contact-temperature-sensor"), - require("multi-sensor"), - require("smartsense-multi"), - require("sengled"), - require("frient") - }, + sub_drivers = require("sub_drivers"), ias_zone_configuration_method = constants.IAS_ZONE_CONFIGURE_TYPE.AUTO_ENROLL_RESPONSE, health_check = false, } diff --git a/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua new file mode 100644 index 0000000000..0065632f88 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_multi_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("multi-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("multi-sensor") + end + end + return false +end + +return can_handle_zigbee_multi_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua new file mode 100644 index 0000000000..94e73b1691 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function centralite_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "CentraLite" then + return true, require("multi-sensor.centralite-multi") + end + return false +end + +return centralite_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua index 1e75aa6b0d..0f7b7b7f25 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/centralite-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local multi_utils = require "multi-sensor/multi_utils" @@ -67,9 +57,7 @@ local centralite_handler = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "CentraLite" - end + can_handle = require("multi-sensor.centralite-multi.can_handle"), } return centralite_handler diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua new file mode 100644 index 0000000000..7abfe772b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local MULTI_SENSOR_FINGERPRINTS = { + { mfr = "CentraLite", model = "3320" }, + { mfr = "CentraLite", model = "3321" }, + { mfr = "CentraLite", model = "3321-S" }, + { mfr = "SmartThings", model = "multiv4" }, + { mfr = "Samjin", model = "multi" }, + { mfr = "Third Reality, Inc", model = "3RVS01031Z" } +} + +return MULTI_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua index a440156c11..4cc8f88efa 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local zcl_commands = require "st.zigbee.zcl.global_commands" local multi_utils = require "multi-sensor/multi_utils" @@ -18,23 +9,7 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local contactSensor_defaults = require "st.zigbee.defaults.contactSensor_defaults" local capabilities = require "st.capabilities" -local MULTI_SENSOR_FINGERPRINTS = { - { mfr = "CentraLite", model = "3320" }, - { mfr = "CentraLite", model = "3321" }, - { mfr = "CentraLite", model = "3321-S" }, - { mfr = "SmartThings", model = "multiv4" }, - { mfr = "Samjin", model = "multi" }, - { mfr = "Third Reality, Inc", model = "3RVS01031Z" } -} -local function can_handle_zigbee_multi_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(MULTI_SENSOR_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function multi_sensor_report_handler(driver, device, zb_rx) local x, y, z @@ -98,12 +73,7 @@ local multi_sensor = { } } }, - sub_drivers = { - require("multi-sensor/smartthings-multi"), - require("multi-sensor/samjin-multi"), - require("multi-sensor/centralite-multi"), - require("multi-sensor/thirdreality-multi") - }, - can_handle = can_handle_zigbee_multi_sensor + sub_drivers = require("multi-sensor.sub_drivers"), + can_handle = require("multi-sensor.can_handle"), } return multi_sensor diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua index e24ec1097b..b1b3e18be4 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/multi_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cluster_base = require "st.zigbee.cluster_base" @@ -152,4 +141,4 @@ multi_utils.convert_to_signedInt16 = function(byte1, byte2) end -return multi_utils \ No newline at end of file +return multi_utils diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua new file mode 100644 index 0000000000..e524845f40 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" then + return true, require("multi-sensor.samjin-multi") + end + return false +end + +return samjin_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua index 935d1cfc20..3bc3edbcf9 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/samjin-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local data_types = require "st.zigbee.data_types" @@ -54,9 +44,7 @@ local samjin_driver = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" - end + can_handle = require("multi-sensor.samjin-multi.can_handle"), } return samjin_driver diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua new file mode 100644 index 0000000000..6c99521efc --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function smartthings_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SmartThings" then + return true, require("multi-sensor.smartthings-multi") + end + return false +end + +return smartthings_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua index 69d0b7f3f4..24234ef6de 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/smartthings-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local zcl_commands = require "st.zigbee.zcl.global_commands" @@ -91,9 +81,7 @@ local smartthings_multi = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SmartThings" - end + can_handle = require("multi-sensor.smartthings-multi.can_handle"), } return smartthings_multi diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/sub_drivers.lua new file mode 100644 index 0000000000..3769ab89d1 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/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("multi-sensor/smartthings-multi"), + lazy_load_if_possible("multi-sensor/samjin-multi"), + lazy_load_if_possible("multi-sensor/centralite-multi"), + lazy_load_if_possible("multi-sensor/thirdreality-multi"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua new file mode 100644 index 0000000000..bdfc4f75b5 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_multi_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" then + return true, require("multi-sensor.thirdreality-multi") + end + return false +end + +return thirdreality_multi_can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua index c63596cab5..1be7988fac 100644 --- a/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/multi-sensor/thirdreality-multi/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_commands = require "st.zigbee.zcl.global_commands" local multi_utils = require "multi-sensor/multi_utils" @@ -47,9 +37,7 @@ local thirdreality_multi = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" - end + can_handle = require("multi-sensor.thirdreality-multi.can_handle"), } return thirdreality_multi diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua new file mode 100644 index 0000000000..68d774fb8f --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sengled/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sengled_products = function(opts, driver, device, ...) + local FINGERPRINTS = require("sengled.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sengled") + end + end + return false +end + +return is_sengled_products diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua new file mode 100644 index 0000000000..4d015324b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sengled/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "sengled", model = "E2D-G73" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua index 95b6e5e3e8..6a4b4c6814 100644 --- a/drivers/SmartThings/zigbee-contact/src/sengled/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/sengled/init.lua @@ -1,12 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration -local FINGERPRINTS = { - { mfr = "sengled", model = "E2D-G73" } -} local CONFIGURATIONS = { { @@ -27,14 +27,6 @@ local CONFIGURATIONS = { } } -local is_sengled_products = function(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_init(driver, device) battery_defaults.build_linear_voltage_init(2.1, 3.0)(driver, device) @@ -49,7 +41,7 @@ local sengled_contact_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_sengled_products + can_handle = require("sengled.can_handle"), } return sengled_contact_handler diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua new file mode 100644 index 0000000000..f5c226cc55 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local SMARTSENSE_PROFILE_ID = 0xFC01 + local FINGERPRINTS = require("smartsense-multi.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("smartsense-multi") + end + end + local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] + if endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua new file mode 100644 index 0000000000..fff1b601a3 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SMARTSENSE_MULTI_FINGERPRINTS = { + { mfr = "SmartThings", model = "PGC313" }, + { mfr = "SmartThings", model = "PGC313EU" } +} + +return SMARTSENSE_MULTI_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua index cbe59d7c09..d5030278e8 100644 --- a/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/smartsense-multi/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local multi_utils = require "multi-sensor/multi_utils" @@ -24,23 +13,6 @@ local SMARTSENSE_MULTI_ACC_CMD = 0x00 local SMARTSENSE_MULTI_XYZ_CMD = 0x05 local SMARTSENSE_MULTI_STATUS_CMD = 0x07 local SMARTSENSE_MULTI_STATUS_REPORT_CMD = 0x09 -local SMARTSENSE_PROFILE_ID = 0xFC01 - -local SMARTSENSE_MULTI_FINGERPRINTS = { - { mfr = "SmartThings", model = "PGC313" }, - { mfr = "SmartThings", model = "PGC313EU" } -} - -local function can_handle(opts, driver, device, ...) - for _, fingerprint in ipairs(SMARTSENSE_MULTI_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - local endpoint = device.zigbee_endpoints[1] or device.zigbee_endpoints["1"] - if endpoint.profile_id == SMARTSENSE_PROFILE_ID then return true end - return false -end local function acceleration_handler(driver, device, zb_rx) -- This is a custom cluster command for the kickstarter multi. @@ -182,7 +154,7 @@ local smartsense_multi = { } } }, - can_handle = can_handle + can_handle = require("smartsense-multi.can_handle"), } return smartsense_multi diff --git a/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua b/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua new file mode 100644 index 0000000000..394de5a72d --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/sub_drivers.lua @@ -0,0 +1,14 @@ +-- 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("aqara"), + lazy_load_if_possible("aurora-contact-sensor"), + lazy_load_if_possible("contact-temperature-sensor"), + lazy_load_if_possible("multi-sensor"), + lazy_load_if_possible("smartsense-multi"), + lazy_load_if_possible("sengled"), + lazy_load_if_possible("frient"), +} +return sub_drivers 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 119e3fbdab..7f2dae99fe 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" 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 40cc2c47be..46fc40f686 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 e3628f6ec7..d914712e06 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 e05233ad48..075b923bed 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 a05f420166..716828541a 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 58c2c8d6b2..f983211856 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 7d1319abc0..bd7e7512dd 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 12bfbc0b1e..4b0363caba 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d9671a5283..6a7876541f 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 458af19546..291755bb17 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d8a23f4af5..2e0c67469d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 1950a22649..39f127dcf4 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 9db8033062..035c49d27f 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -437,4 +426,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 05e5fdd0d4..5ef40d5aaf 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 07beb7c0dd..a6d989609f 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d7076ec9ee..8db97fd224 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 44570bcce1..5e3404b2a0 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 e288d3a927..0ee62b73ee 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" From a30b7c04915e948e8ed60dc62d1c25241490d047 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:24:39 -0600 Subject: [PATCH 412/449] Matter Sensor: fix gating for modular profile update (#2766) The `MODULAR_PROFILE_UPDATED` flag check in infoChanged could cause a subscription request to be sent out before the actual profile update takes place if the infoChanged event was caused by something other than a profile update. This change adds a function to check if the profile was actually updated before sending the subscription request. --- .../device_configuration.lua | 1 - .../air_quality_sensor_utils/fields.lua | 2 -- .../air_quality_sensor_utils/utils.lua | 18 ++++++++++++++++++ .../sub_drivers/air_quality_sensor/init.lua | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) 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 6e1f1a5d81..a30eaed0f8 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 @@ -71,7 +71,6 @@ function DeviceConfiguration.match_profile(device) end device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) - device:set_field(fields.MODULAR_PROFILE_UPDATED, true) -- 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. 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 0d3c5dd21c..1e4658c99e 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 @@ -26,8 +26,6 @@ local AirQualitySensorFields = {} AirQualitySensorFields.AIR_QUALITY_SENSOR_DEVICE_TYPE_ID = 0x002C -AirQualitySensorFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" - AirQualitySensorFields.SUPPORTED_COMPONENT_CAPABILITIES = "__supported_component_capabilities" AirQualitySensorFields.units_required = { 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 1a36f5f920..95ca80964c 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 @@ -77,4 +77,22 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device) end end +function AirQualitySensorUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then + return true + end + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return true + end + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then + return true + end + end + end + return false +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 61280fd107..98b8430c98 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 @@ -66,12 +66,12 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) end function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then + if device.profile.id ~= args.old_st_store.profile.id or + aqs_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) 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) end - device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) aqs_utils.set_supported_health_concern_values(device) device:subscribe() end From cd71bb442ef05b6e8cad005a78f0052ac65e2a6f Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:34:52 -0600 Subject: [PATCH 413/449] Matter Camera: Fix gating of videoStreamSettings capability (#2769) `videoStreamSettings` should be included in the camera profile if the Camera AVSM cluster is available and the VDO feature is enabled. It's currently gated by the presence of the Camera AVSULM cluster which is incorrect. --- .../camera/camera_utils/device_configuration.lua | 2 +- .../matter-switch/src/test/test_matter_camera.lua | 10 +++++----- 2 files changed, 6 insertions(+), 6 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 c84337736d..20641ebd33 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 @@ -66,6 +66,7 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.videoCapture2.ID) end table.insert(main_component_capabilities, capabilities.cameraViewportSettings.ID) + table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.LOCAL_STORAGE) then table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) @@ -103,7 +104,6 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) end - table.insert(main_component_capabilities, capabilities.videoStreamSettings.ID) 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.OccupancySensing.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 1028197590..db8d980fab 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -239,6 +239,7 @@ local expected_metadata = { { "videoCapture2", "cameraViewportSettings", + "videoStreamSettings", "localMediaStorage", "audioRecording", "cameraPrivacyMode", @@ -246,7 +247,6 @@ local expected_metadata = { "hdr", "nightVision", "mechanicalPanTiltZoom", - "videoStreamSettings", "zoneManagement", "webrtc", "motionSensor", @@ -1857,11 +1857,11 @@ test.register_coroutine_test( uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID) }) }) - local expected_metadata = { + local updated_expected_metadata = { optional_component_capabilities = { { "main", - { "videoCapture2", "cameraViewportSettings", "localMediaStorage", "audioRecording", "cameraPrivacyMode", - "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "videoStreamSettings", "zoneManagement", + { "videoCapture2", "cameraViewportSettings", "videoStreamSettings", "localMediaStorage", "audioRecording", + "cameraPrivacyMode", "imageControl", "hdr", "nightVision", "mechanicalPanTiltZoom", "zoneManagement", "webrtc", "motionSensor", "sounds", } }, { "statusLed", @@ -1879,7 +1879,7 @@ test.register_coroutine_test( }, profile = "camera" } - mock_device:expect_metadata_update(expected_metadata) + mock_device:expect_metadata_update(updated_expected_metadata) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) end From 5e9fdc86809e3807951a5506142ecce48d9bbef9 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:36 -0600 Subject: [PATCH 414/449] CHAD-17067: zigbee-fan lazy load subdrivers --- .../zigbee-fan/src/configurations.lua | 15 ++------- .../zigbee-fan/src/fan-light/can_handle.lua | 14 +++++++++ .../zigbee-fan/src/fan-light/fingerprints.lua | 8 +++++ .../zigbee-fan/src/fan-light/init.lua | 31 +++---------------- drivers/SmartThings/zigbee-fan/src/init.lua | 23 +++----------- .../zigbee-fan/src/lazy_load_subdriver.lua | 15 +++++++++ .../zigbee-fan/src/sub_drivers.lua | 8 +++++ .../zigbee-fan/src/test/test_fan_light.lua | 16 ++-------- 8 files changed, 59 insertions(+), 71 deletions(-) create mode 100644 drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-fan/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-fan/src/configurations.lua b/drivers/SmartThings/zigbee-fan/src/configurations.lua index c2cbb57de0..f6bcbe7bf5 100644 --- a/drivers/SmartThings/zigbee-fan/src/configurations.lua +++ b/drivers/SmartThings/zigbee-fan/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua new file mode 100644 index 0000000000..46523f65f9 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_itm_fanlight(opts, driver, device) + local FINGERPRINTS = require("fan-light.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("fan-light") + end + end + return false +end + +return can_handle_itm_fanlight diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua new file mode 100644 index 0000000000..bf93802c10 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-003" }, +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua index 95329c1ddf..94d44bfde3 100644 --- a/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/fan-light/init.lua @@ -1,34 +1,13 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local FanControl = clusters.FanControl local Level = clusters.Level local OnOff = clusters.OnOff -local FINGERPRINTS = { - { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-003" }, -} -local function can_handle_itm_fanlight(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 -- CAPABILITY HANDLERS @@ -111,9 +90,7 @@ local itm_fan_light = { [capabilities.fanSpeed.commands.setFanSpeed.NAME] = fan_speed_handler } }, - can_handle = can_handle_itm_fanlight + can_handle = require("fan-light.can_handle"), } return itm_fan_light - - diff --git a/drivers/SmartThings/zigbee-fan/src/init.lua b/drivers/SmartThings/zigbee-fan/src/init.lua index a34e90ff5e..c4a7185838 100644 --- a/drivers/SmartThings/zigbee-fan/src/init.lua +++ b/drivers/SmartThings/zigbee-fan/src/init.lua @@ -1,23 +1,13 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" -local configurationMap = require "configurations" local device_init = function(self, device) + local configurationMap = require "configurations" local configuration = configurationMap.get_device_configuration(device) if configuration ~= nil then for _, attribute in ipairs(configuration) do @@ -32,9 +22,7 @@ local zigbee_fan_driver = { capabilities.switchLevel, capabilities.fanspeed }, - sub_drivers = { - require("fan-light") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init }, @@ -44,4 +32,3 @@ local zigbee_fan_driver = { defaults.register_for_default_handlers(zigbee_fan_driver,zigbee_fan_driver.supported_capabilities) local fan = ZigbeeDriver("zigbee-fan", zigbee_fan_driver) fan:run() - diff --git a/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua b/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua new file mode 100644 index 0000000000..f43392d622 --- /dev/null +++ b/drivers/SmartThings/zigbee-fan/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("fan-light"), +} +return sub_drivers 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 4ef4ce37e6..0fa987e35a 100644 --- a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua +++ b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 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" From 10586b2cdf1caad2446aaca91cf0bc6754e28b1a Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:18 -0600 Subject: [PATCH 415/449] CHAD-17069: zigbee-illuminance lazy load sub drivers --- .../src/aqara/can_handle.lua | 14 +++++++++++++ .../src/aqara/fingerprints.lua | 8 ++++++++ .../src/aqara/init.lua | 16 ++++----------- .../zigbee-illuminance-sensor/src/init.lua | 20 ++++--------------- .../src/lazy_load_subdriver.lua | 15 ++++++++++++++ .../src/sub_drivers.lua | 8 ++++++++ .../src/test/test_illuminance_sensor.lua | 15 ++------------ .../test/test_illuminance_sensor_aqara.lua | 15 ++------------ 8 files changed, 57 insertions(+), 54 deletions(-) create mode 100644 drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..69218a197a --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.sen_ill.agl01" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua index 72fcbc2489..0eb5e1b059 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/aqara/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zcl_commands = require "st.zigbee.zcl.global_commands" @@ -19,9 +22,6 @@ local FREQUENCY_ATTRIBUTE_ID = 0x0000 local FREQUENCY_DEFAULT_VALUE = 5 local FREQUENCY_PREF = "frequencyPref" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.sen_ill.agl01" } -} local configuration = { { @@ -42,14 +42,6 @@ local configuration = { } } -local function is_aqara_products(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 detection_frequency_capability_handler(driver, device, command) local frequency = command.args.frequency @@ -104,7 +96,7 @@ local aqara_illuminance_handler = { } } }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_illuminance_handler diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua index 635503c232..7f84eb68dc 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -26,12 +16,10 @@ local zigbee_illuminance_driver = { capabilities.illuminanceMeasurement, capabilities.battery }, - sub_drivers = { - require("aqara") - }, lifecycle_handlers = { doConfigure = do_configure, }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..6211f46578 --- /dev/null +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("aqara"), +} +return sub_drivers 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 1987964075..92e93119f5 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 11d66478ba..557d4e2345 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" From 42658b056d2561b17b761915c24959506cf8636d Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:35:29 -0600 Subject: [PATCH 416/449] Matter Thermostat: Add 'off' to thermostatMode by default only if switch is not supported (#2761) --- .../src/test/test_matter_room_ac.lua | 84 +++++++++++++++++++ .../attribute_handlers.lua | 8 +- 2 files changed, 90 insertions(+), 2 deletions(-) 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 633fb3f1c1..bb7b9e6bde 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 @@ -481,4 +481,88 @@ test.register_message_test( } ) +local ControlSequenceOfOperation = clusters.Thermostat.attributes.ControlSequenceOfOperation +test.register_message_test( + "Room AC control sequence reports should generate correct messages", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.COOLING_AND_HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.HEATING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + ControlSequenceOfOperation:build_test_report_data(mock_device, 1, ControlSequenceOfOperation.COOLING_WITH_REHEAT) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool"}, {visibility={displayed=false}})) + }, + } +) + +test.register_message_test( + "Additional mode reports should extend the supported modes", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Thermostat.server.attributes.ControlSequenceOfOperation:build_test_report_data(mock_device, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat"}, {visibility={displayed=false}})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.Thermostat.server.attributes.SystemMode:build_test_report_data(mock_device, 1, 5) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.supportedThermostatModes({"cool", "heat", "emergency heat"}, {visibility={displayed=false}})) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.emergency_heat()) + } + } +) + + 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 78f5ee3bf2..f0a210bc16 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -84,9 +84,13 @@ end function AttributeHandlers.control_sequence_of_operation_handler(driver, device, ib, response) -- The ControlSequenceOfOperation attribute only directly specifies what can't be operated by the operating environment, not what can. -- However, we assert here that a Cooling enum value implies that SystemMode supports cooling, and the same for a Heating enum. - -- We also assert that Off is supported, though per spec this is optional. + -- We also assert that Off is supported if the switch capability is not supported, though per spec this is optional. if device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN) == nil then - device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) + if device:supports_capability(capabilities.switch) == false then + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {capabilities.thermostatMode.thermostatMode.off.NAME}, {persist=true}) + else + device:set_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN, {}, {persist=true}) + end end local supported_modes = st_utils.deep_copy(device:get_field(fields.OPTIONAL_THERMOSTAT_MODES_SEEN)) local disallowed_mode_operations = {} From 133c29a527124b2a79896884a2f0013288d4d4d3 Mon Sep 17 00:00:00 2001 From: Dawid Liberda <157011002+daveylib-abb@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:34:45 +0100 Subject: [PATCH 417/449] Introduction of the LUA Edge Driver for ABB SCU200 InSite Energy Management System (#2190) * Introduction of the LUA Edge Driver for ABB SCU200 InSite Energy Managament System * Added powerConsumptionReport capability to the INS-E3 product * Implementing suggestions made by the SmartThings Energy team * Removed unnecessary device profiles * Fixed detecting type of current sensors and removed support of production current sensors * Fixed all the warnings from the Luacheck linter tool * Moved the abb-scu200 driver to the ABB directory and renamed to insite-scu200 --- drivers/ABB/insite-scu200/config.yml | 7 + .../profiles/auxiliary-contact.yml | 34 ++ drivers/ABB/insite-scu200/profiles/bridge.yml | 8 + .../profiles/current-sensor-consumption.yml | 16 + .../profiles/current-sensor-production.yml | 16 + .../insite-scu200/profiles/energy-meter.yml | 44 ++ .../ABB/insite-scu200/profiles/gas-meter.yml | 10 + .../insite-scu200/profiles/output-module.yml | 10 + .../profiles/usb-energy-meter.yml | 14 + .../insite-scu200/profiles/water-meter.yml | 10 + .../ABB/insite-scu200/search-parameters.yaml | 2 + drivers/ABB/insite-scu200/src/abb/api.lua | 145 +++++ .../insite-scu200/src/abb/device_manager.lua | 115 ++++ .../src/abb/device_refresher.lua | 382 +++++++++++++ drivers/ABB/insite-scu200/src/commands.lua | 111 ++++ drivers/ABB/insite-scu200/src/config.lua | 72 +++ .../insite-scu200/src/connection_monitor.lua | 71 +++ drivers/ABB/insite-scu200/src/discovery.lua | 315 +++++++++++ .../insite-scu200/src/eventsource_handler.lua | 128 +++++ drivers/ABB/insite-scu200/src/fields.lua | 16 + drivers/ABB/insite-scu200/src/init.lua | 45 ++ drivers/ABB/insite-scu200/src/lifecycles.lua | 87 +++ .../ABB/insite-scu200/src/lunchbox/init.lua | 4 + .../ABB/insite-scu200/src/lunchbox/rest.lua | 427 ++++++++++++++ .../src/lunchbox/sse/eventsource.lua | 523 ++++++++++++++++++ .../ABB/insite-scu200/src/lunchbox/util.lua | 46 ++ drivers/ABB/insite-scu200/src/utils.lua | 219 ++++++++ 27 files changed, 2877 insertions(+) create mode 100644 drivers/ABB/insite-scu200/config.yml create mode 100644 drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml create mode 100644 drivers/ABB/insite-scu200/profiles/bridge.yml create mode 100644 drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml create mode 100644 drivers/ABB/insite-scu200/profiles/current-sensor-production.yml create mode 100644 drivers/ABB/insite-scu200/profiles/energy-meter.yml create mode 100644 drivers/ABB/insite-scu200/profiles/gas-meter.yml create mode 100644 drivers/ABB/insite-scu200/profiles/output-module.yml create mode 100644 drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml create mode 100644 drivers/ABB/insite-scu200/profiles/water-meter.yml create mode 100644 drivers/ABB/insite-scu200/search-parameters.yaml create mode 100644 drivers/ABB/insite-scu200/src/abb/api.lua create mode 100644 drivers/ABB/insite-scu200/src/abb/device_manager.lua create mode 100644 drivers/ABB/insite-scu200/src/abb/device_refresher.lua create mode 100644 drivers/ABB/insite-scu200/src/commands.lua create mode 100644 drivers/ABB/insite-scu200/src/config.lua create mode 100644 drivers/ABB/insite-scu200/src/connection_monitor.lua create mode 100644 drivers/ABB/insite-scu200/src/discovery.lua create mode 100644 drivers/ABB/insite-scu200/src/eventsource_handler.lua create mode 100644 drivers/ABB/insite-scu200/src/fields.lua create mode 100644 drivers/ABB/insite-scu200/src/init.lua create mode 100644 drivers/ABB/insite-scu200/src/lifecycles.lua create mode 100644 drivers/ABB/insite-scu200/src/lunchbox/init.lua create mode 100644 drivers/ABB/insite-scu200/src/lunchbox/rest.lua create mode 100644 drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua create mode 100644 drivers/ABB/insite-scu200/src/lunchbox/util.lua create mode 100644 drivers/ABB/insite-scu200/src/utils.lua diff --git a/drivers/ABB/insite-scu200/config.yml b/drivers/ABB/insite-scu200/config.yml new file mode 100644 index 0000000000..2cda5384d4 --- /dev/null +++ b/drivers/ABB/insite-scu200/config.yml @@ -0,0 +1,7 @@ +name: 'SCU200 InSite Energy Management System' +packageKey: 'ABB.SCU200' +description: "SmartThings driver for SCU200 InSite Energy Management System" +vendorSupportInformation: "https://support.smartthings.com" +permissions: + lan: {} + discovery: {} \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml b/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml new file mode 100644 index 0000000000..92c4a2fcb6 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/auxiliary-contact.yml @@ -0,0 +1,34 @@ +name: abb.scu200.auxiliary-contact.v1 +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch +deviceConfig: + dashboard: + states: + - component: main + capability: switch + version: 1 + actions: [] + detailView: + - component: main + capability: switch + version: 1 + visibleCondition: + capability: switch + version: 1 + component: main + value: switch.value + operator: ONE_OF + operand: '[""]' + automation: + conditions: + - component: main + capability: switch + version: 1 + actions: [] \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/bridge.yml b/drivers/ABB/insite-scu200/profiles/bridge.yml new file mode 100644 index 0000000000..d45ebc4943 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/bridge.yml @@ -0,0 +1,8 @@ +name: abb.scu200.bridge.v1 +components: +- id: main + capabilities: + - id: refresh + version: 1 + categories: + - name: Bridges diff --git a/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml b/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml new file mode 100644 index 0000000000..5843dae90f --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/current-sensor-consumption.yml @@ -0,0 +1,16 @@ +name: abb.scu200.current-sensor-consumption.v1 +components: +- id: main + capabilities: + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml b/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml new file mode 100644 index 0000000000..ec4e12aeae --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/current-sensor-production.yml @@ -0,0 +1,16 @@ +name: abb.scu200.current-sensor-production.v1 +components: +- id: main + capabilities: + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/energy-meter.yml b/drivers/ABB/insite-scu200/profiles/energy-meter.yml new file mode 100644 index 0000000000..37fab87dbe --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/energy-meter.yml @@ -0,0 +1,44 @@ +name: abb.scu200.energy-meter.v1 +components: +- id: main + capabilities: + - id: voltageMeasurement + version: 1 + - id: currentMeasurement + version: 1 + - id: powerMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter +- id: consumptionMeter + label: "From Grid" + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: productionMeter + label: "To Grid" + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter +- id: surplus + capabilities: + - id: powerConsumptionReport + version: 1 + - id: energyMeter + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/gas-meter.yml b/drivers/ABB/insite-scu200/profiles/gas-meter.yml new file mode 100644 index 0000000000..cc3297eb05 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/gas-meter.yml @@ -0,0 +1,10 @@ +name: abb.scu200.gas-meter.v1 +components: +- id: main + capabilities: + - id: gasMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Others \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/output-module.yml b/drivers/ABB/insite-scu200/profiles/output-module.yml new file mode 100644 index 0000000000..d6e0395da8 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/output-module.yml @@ -0,0 +1,10 @@ +name: abb.scu200.output-module.v1 +components: +- id: main + capabilities: + - id: switch + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch diff --git a/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml b/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml new file mode 100644 index 0000000000..8eb09ce365 --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/usb-energy-meter.yml @@ -0,0 +1,14 @@ +name: abb.scu200.usb-energy-meter.v1 +components: +- id: main + capabilities: + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: powerConsumptionReport + version: 1 + - id: refresh + version: 1 + categories: + - name: CurbPowerMeter \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/profiles/water-meter.yml b/drivers/ABB/insite-scu200/profiles/water-meter.yml new file mode 100644 index 0000000000..bf5f50068d --- /dev/null +++ b/drivers/ABB/insite-scu200/profiles/water-meter.yml @@ -0,0 +1,10 @@ +name: abb.scu200.water-meter.v1 +components: +- id: main + capabilities: + - id: waterMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Others \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/search-parameters.yaml b/drivers/ABB/insite-scu200/search-parameters.yaml new file mode 100644 index 0000000000..5cfaf45c83 --- /dev/null +++ b/drivers/ABB/insite-scu200/search-parameters.yaml @@ -0,0 +1,2 @@ +ssdp: + - searchTerm: urn:ABB:device:SCU200:1 \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/api.lua b/drivers/ABB/insite-scu200/src/abb/api.lua new file mode 100644 index 0000000000..6524d86567 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/api.lua @@ -0,0 +1,145 @@ +local log = require("log") +local st_utils = require "st.utils" +local json = require "st.json" + +-- Local imports +local config = require("config") +local utils = require("utils") +local RestClient = require "lunchbox.rest" + +-- API for the ABB SCU200 Bridge +local api = {} +api.__index = api + +local SSL_CONFIG = { + mode = "client", + protocol = "any", + verify = "none", + options = "all" +} + +local ADDITIONAL_HEADERS = { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", +} + +-- Method for getting the base URL +local function get_base_url(bridge_ip) + return "https://" .. bridge_ip .. ":" .. config.REST_API_PORT +end + +-- Method for processing the REST response +local function process_rest_response(response, err, partial) + if err ~= nil then + return response, err, nil + elseif response ~= nil then + local status, decoded_json = pcall(json.decode, response:get_body()) + + if status and response.status == 200 then + log.debug("process_rest_response(): Response = " .. response.status .. " " .. response:get_body()) + + return decoded_json, nil, response.status + elseif status then + log.error("process_rest_response(): Response error = " .. response.status) + + return nil, "response status is not 200 OK", response.status + else + log.error("process_rest_response(): Failed to decode data") + + return nil, "failed to decode data", nil + end + else + return nil, "no response or error received", nil + end +end + +-- Method for creating a retry function +local function retry_fn(retry_attempts) + local count = 0 + + return function() + count = count + 1 + return count < retry_attempts + end +end + +-- Method for performing a GET request +local function do_get(api_instance, path) + log.debug("do_get(): Sending GET request to " .. path) + + return process_rest_response(api_instance.client:get(path, api_instance.headers, retry_fn(5))) +end + +-- Method for performing a POST request +local function do_post(api_instance, path, payload) + log.debug("do_post(): Sending POST request to " .. path .. " with payload " .. json.encode(payload)) + + return process_rest_response(api_instance.client:post(path, payload, api_instance.headers, retry_fn(5))) +end + +-- Method for creating a labeled socket builder +function api.labeled_socket_builder(label) + local socket_builder = utils.labeled_socket_builder(label, SSL_CONFIG) + + return socket_builder +end + +-- Method for creating a new bridge manager +function api.new_bridge_manager(bridge_ip, bridge_dni) + local base_url = get_base_url(bridge_ip) + local socket_builder = api.labeled_socket_builder(bridge_dni) + + return setmetatable( + { + headers = st_utils.deep_copy(ADDITIONAL_HEADERS), + client = RestClient.new(base_url, socket_builder), + base_url = base_url + }, + api + ) +end + +-- Method for getting the thing infos +function api.get_thing_infos(bridge_ip, bridge_dni) + local socket_builder = api.labeled_socket_builder(bridge_dni .. " (thing infos)") + local response, error, status = process_rest_response(RestClient.one_shot_get(get_base_url(bridge_ip) .. "/devices", ADDITIONAL_HEADERS, socket_builder)) + + if not error and status == 200 then + return response + else + log.error("api.get_thing_infos(): Failed to get thing infos, error = " .. error) + return nil + end +end + +-- Method for getting the bridge info +function api.get_bridge_info(bridge_ip, bridge_dni) + local socket_builder = api.labeled_socket_builder(bridge_dni .. " (bridge info)") + local response, error, status = process_rest_response(RestClient.one_shot_get(get_base_url(bridge_ip) .. "/bridge", ADDITIONAL_HEADERS, socket_builder)) + + if not error and status == 200 then + return response + else + log.error("api.get_bridge_info(): Failed to get thing infos, error = " .. error) + return nil + end +end + +-- API methods +function api:get_devices() + return do_get(self, "/devices") +end + +function api:get_device_by_id(id) + return do_get(self, string.format("/devices/%s", id)) +end + +function api:post_device_by_id(id, payload) + return do_post(self, string.format("/devices/%s/control", id), payload) +end + +function api:get_sse_url() + return self.base_url .. "/events" +end + +return api \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/device_manager.lua b/drivers/ABB/insite-scu200/src/abb/device_manager.lua new file mode 100644 index 0000000000..a9de6c0811 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/device_manager.lua @@ -0,0 +1,115 @@ +local log = require("log") + +-- Local imports +local utils = require("utils") +local fields = require("fields") +local api = require("abb.api") +local device_refresher = require("abb.device_refresher") + +-- Device manager methods +local device_manager = {} + +-- Method for checking if connection is valid +function device_manager.is_valid_connection(driver, device, conn_info) + local dni = utils.get_dni_from_device(device) + + if not conn_info then + log.error("device_manager.is_valid_connection(): Failed to find conn_info, dni = " .. dni) + return false + end + + local bridge_ip = utils.get_device_ip_address(device) + local thing_infos = api.get_thing_infos(bridge_ip, dni) + + if thing_infos and thing_infos.devices then + return true + else + log.error("device_manager.is_valid_connection(): Failed to get thing infos, dni = " .. dni) + return false + end +end + +-- Method for getting bridge connection info +function device_manager.get_bridge_connection_info(driver, bridge_dni, bridge_ip) + local bridge_conn_info = api.new_bridge_manager(bridge_ip, bridge_dni) + + if bridge_conn_info == nil then + log.error("device_manager.get_bridge_connection_info(): No bridge connection info") + end + + return bridge_conn_info +end + +-- Method for handling JSON status +function device_manager.handle_device_json(driver, device, device_json) + local dni = utils.get_dni_from_device(device) + if dni == nil then + log.error("device_manager.handle_device_json(): dni is nil, the device has been probably deleted") + return + end + + if not device_json then + log.error("device_manager.handle_device_json(): device_json is nil, dni = " .. dni) + return + end + + log.debug("device_manager.handle_device_json(): dni: " .. dni .. " device_json = " .. utils.dump(device_json)) + + local status = device_json.status + if status ~= nil then + if status == "offline" then + log.info("device_manager.handle_device_json(): status is offline, dni = " .. dni) + + device:offline() + return + elseif status == "online" then + device:online() + end + end + + local values = device_json.values + if values == nil then + log.error("device_manager.handle_device_json(): values is nil, dni = " .. dni) + return + end + + device_refresher.refresh_device(driver, device, values) +end + +-- Method for refreshing device +function device_manager.refresh(driver, device) + local dni = utils.get_dni_from_device(device) + local communication_device = device:get_parent_device() or device + local conn_info = communication_device:get_field(fields.CONN_INFO) + + if not conn_info then + log.warn("device_manager.refresh(): Failed to find conn_info, dni = " .. dni) + return + end + + local response, err, status = conn_info:get_device_by_id(dni) + + if err or status ~= 200 then + status = status or "nil" + log.error("device_manager.refresh(): Failed to get device by id, dni = " .. dni .. ", err = " .. err .. ", status = " .. status) + + device:offline() + + return + end + + device_manager.handle_device_json(driver, device, response) +end + +-- Method for monitoring the connection of the bridge devices +function device_manager.bridge_monitor(driver, device, bridge_info) + local child_devices = device:get_child_list() + + for _, thing_device in ipairs(child_devices) do + device.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + device_manager.refresh(driver, thing_device) + end) + end +end + +return device_manager \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/abb/device_refresher.lua b/drivers/ABB/insite-scu200/src/abb/device_refresher.lua new file mode 100644 index 0000000000..628c5b4201 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/abb/device_refresher.lua @@ -0,0 +1,382 @@ +local log = require("log") +local caps = require('st.capabilities') + +-- Local imports +local utils = require("utils") +local config = require("config") +local fields = require('fields') + +-- Controller for refreshing device data +local device_refresher = {} + +local function refresh_current_sensor(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_current_sensor(): Refreshing data of Current Sensor, dni = " .. dni) + + -- Refresh Current Measurement + local current = values.current + + if current ~= nil then + log.trace("refresh_current_sensor(): Refreshing Current Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.currentMeasurement.current({value=current, unit="A"})) + end + + -- Refresh Active Power + local activePower = values.activePower + + if activePower ~= nil then + log.trace("refresh_current_sensor(): Refreshing Active Power, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerMeter.power({value=activePower, unit="W"})) + end + + -- Refresh Active Energy + local activeEnergy = values.activeEnergy + + if activeEnergy ~= nil then + -- Refresh Active Energy + log.trace("refresh_current_sensor(): Refreshing Active Energy, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.energyMeter.energy({value=activeEnergy, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy values + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy consumption/production periodically + local current_consumption_production_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta consumption/production energy + local delta_consumption_production_report = 0.0 + if current_consumption_production_report ~= nil then + delta_consumption_production_report = math.max((activeEnergy * 1000) - current_consumption_production_report.energy, 0.0) + end + + -- Refresh Power Energy Report + log.trace("refresh_current_sensor(): Refreshing Energy Report, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergy * 1000, deltaEnergy=delta_consumption_production_report})) + + -- Save date of the last energy report + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_current_sensor(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +local function refresh_energy_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_energy_meter(): Refreshing data of Energy Meter, dni = " .. dni) + + -- Refresh Voltage Measurement + local voltage = values.voltage + + if voltage ~= nil then + log.trace("refresh_energy_meter(): Refreshing Voltage Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.voltageMeasurement.voltage({value=voltage, unit="V"})) + end + + -- Refresh Current Measurement + local current = values.current + + if current ~= nil then + log.trace("refresh_energy_meter(): Refreshing Current Measurement, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.currentMeasurement.current({value=current, unit="A"})) + end + + -- Refresh Active Power + local activePower = values.activePowerTotal + + if activePower ~= nil then + log.trace("refresh_energy_meter(): Refreshing Active Power, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerMeter.power({value=activePower, unit="W"})) + end + + -- Refresh Active Energy Net, Import & Export Total + local activeEnergyNetTotal = values.activeEnergyNetTotal + local activeEnergyImportTotal = values.activeEnergyImportTotal + local activeEnergyExportTotal = values.activeEnergyExportTotal + + if activeEnergyNetTotal == nil then + if activeEnergyImportTotal ~= nil and activeEnergyExportTotal ~= nil then + -- If only Import and Export Total are available, calculate Net Total + activeEnergyNetTotal = activeEnergyImportTotal - activeEnergyExportTotal + end + end + + local activeEnergyNetPositive = math.max(activeEnergyNetTotal, 0.0) + local activeEnergyNetNegative = math.min(activeEnergyNetTotal, 0.0) * -1 -- Convert to positive value + + if activeEnergyNetTotal ~= nil and activeEnergyImportTotal ~= nil and activeEnergyExportTotal ~= nil then + -- Refresh Active Energy Net Positive + log.trace("refresh_energy_meter(): Refreshing Active Energy Net Positive, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.energyMeter.energy({value=activeEnergyNetPositive, unit="kWh"})) + + -- Refresh Active Energy Import Total + log.trace("refresh_energy_meter(): Refreshing Active Energy Import Total, dni = " .. dni) + device.profile.components["consumptionMeter"]:emit_event(caps.energyMeter.energy({value=activeEnergyImportTotal, unit="kWh"})) + + -- Refresh Active Energy Export Total + log.trace("refresh_energy_meter(): Refreshing Active Energy Export Total, dni = " .. dni) + device.profile.components["productionMeter"]:emit_event(caps.energyMeter.energy({value=activeEnergyExportTotal, unit="kWh"})) + + -- Refresh Active Energy Net Negative + log.trace("refresh_energy_meter(): Refreshing Active Energy Net Negative, dni = " .. dni) + device.profile.components["surplus"]:emit_event(caps.energyMeter.energy({value=activeEnergyNetNegative, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy net, consumption and production + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy net, consumption and production periodically + local current_net_positive_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_consumption_report = device:get_latest_state("consumptionMeter", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_production_report = device:get_latest_state("productionMeter", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + local current_net_negative_report = device:get_latest_state("surplus", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta net, consumption and production energy + local delta_net_positive_report = 0.0 + if current_net_positive_report ~= nil then + delta_net_positive_report = math.max((activeEnergyNetPositive * 1000) - current_net_positive_report.energy, 0.0) + end + + local delta_consumption_energy = 0.0 + if current_consumption_report ~= nil then + delta_consumption_energy = math.max((activeEnergyImportTotal * 1000) - current_consumption_report.energy, 0.0) + end + + local delta_production_energy = 0.0 + if current_production_report ~= nil then + delta_production_energy = math.max((activeEnergyExportTotal * 1000) - current_production_report.energy, 0.0) + end + + local delta_net_negative_report = 0.0 + if current_net_negative_report ~= nil then + delta_net_negative_report = math.max((activeEnergyNetNegative * 1000) - current_net_negative_report.energy, 0.0) + end + + -- Refresh Power Net Positive, Consumption, Production & Net Negative Report + log.trace("refresh_energy_meter(): Refreshing Power Net Positive, Consumption, Production & Net Negative Report, dni = " .. dni) + device.profile.components["main"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyNetPositive * 1000, deltaEnergy=delta_net_positive_report})) + device.profile.components["consumptionMeter"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyImportTotal * 1000, deltaEnergy=delta_consumption_energy})) + device.profile.components["productionMeter"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyExportTotal * 1000, deltaEnergy=delta_production_energy})) + device.profile.components["surplus"]:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyNetNegative * 1000, deltaEnergy=delta_net_negative_report})) + + -- Save date of the last consumption + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_energy_meter(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +local function refresh_auxiliary_contact(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_auxiliary_contact(): Refreshing data of Auxiliary Contact, dni = " .. dni) + + -- Refresh Contact Sensor + local isClosed = values.isClosed + + if isClosed ~= nil then + log.trace("refresh_auxiliary_contact(): Refreshing Switch, dni = " .. dni) + + if isClosed == 1 then + isClosed = true + else + isClosed = false + end + + if isClosed then + device:emit_event(caps.switch.switch.on()) + else + device:emit_event(caps.switch.switch.off()) + end + end + + return true +end + +local function refresh_output_module(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_output_module(): Refreshing data of Output Module, dni = " .. dni) + + -- Refresh Switch + local isClosed = values.isClosed + log.trace("refresh_output_module(): Refreshing Switch, dni = " .. dni) + + if isClosed == 1 then + isClosed = true + else + isClosed = false + end + + if isClosed then + device:emit_event(caps.switch.switch.on()) + else + device:emit_event(caps.switch.switch.off()) + end + + return true +end + +local function refresh_water_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_water_meter(): Refreshing data of Water Meter, dni = " .. dni) + + local unit = values.unit + if unit == nil then + log.error("refresh_water_meter(): The unit of the water meter is not set, dni = " .. dni) + return false + end + + -- Refresh Water Meter: last hour + local lastHourFlow = values.lastHourFlow + + if lastHourFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last hour, dni = " .. dni) + device:emit_event(caps.waterMeter.lastHour({value=lastHourFlow, unit=unit})) + end + + -- Refresh Water Meter: last 24 hours + local lastTwentyFourHoursFlow = values.lastTwentyFourHoursFlow + + if lastTwentyFourHoursFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last 24 hours, dni = " .. dni) + device:emit_event(caps.waterMeter.lastTwentyFourHours({value=lastTwentyFourHoursFlow, unit=unit})) + end + + -- Refresh Water Meter: last 7 days + local lastSevenDaysFlow = values.lastSevenDaysFlow + + if lastSevenDaysFlow ~= nil then + log.trace("refresh_water_meter(): Refreshing Water Meter: last 7 days, dni = " .. dni) + device:emit_event(caps.waterMeter.lastSevenDays({value=lastSevenDaysFlow, unit=unit})) + end + + return true +end + +local function refresh_gas_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_gas_meter(): Refreshing data of Gas Meter, dni = " .. dni) + + local gasMeterVolumeUnit = values.gasMeterVolumeUnit + if gasMeterVolumeUnit == nil then + log.error("refresh_gas_meter(): The unit of the gas meter is not set, dni = " .. dni) + return false + end + + -- Correct the unit if necessary + if gasMeterVolumeUnit == "m3" then + gasMeterVolumeUnit = "m^3" + end + + -- Refresh Gas Meter + local gasMeterVolume = values.gasMeterVolume + + if gasMeterVolume ~= nil then + log.trace("refresh_gas_meter(): Refreshing Gas Meter, dni = " .. dni) + device:emit_event(caps.gasMeter.gasMeterVolume({value=gasMeterVolume, unit=gasMeterVolumeUnit})) + end + + return true +end + +local function refresh_usb_energy_meter(driver, device, values) + local dni = utils.get_dni_from_device(device) + log.info("refresh_usb_energy_meter(): Refreshing data of USB Energy Meter, dni = " .. dni) + + -- Refresh Active Power Import Total + local activePowerImportTotal = values.activePowerImportTotal + + if activePowerImportTotal ~= nil then + log.trace("refresh_usb_energy_meter(): Refreshing Active Power Import Total, dni = " .. dni) + device:emit_event(caps.powerMeter.power({value=activePowerImportTotal, unit="W"})) + end + + -- Refresh Active Energy Import Total + local activeEnergyImportTotal = values.activeEnergyImportTotal + + if activeEnergyImportTotal ~= nil then + log.trace("refresh_usb_energy_meter(): Refreshing Active Energy Import Total, dni = " .. dni) + device:emit_event(caps.energyMeter.energy({value=activeEnergyImportTotal, unit="kWh"})) + + -- Verify whether the appropriate time have elapsed to report the energy consumption and production + local last_energy_report = device:get_field(fields.LAST_ENERGY_REPORT) or 0.0 + + if (os.time() - last_energy_report) >= config.EDGE_CHILD_ENERGY_REPORT_INTERVAL then -- Report the energy consumption periodically + local current_consumption_report = device:get_latest_state("main", caps.powerConsumptionReport.ID, caps.powerConsumptionReport.powerConsumption.NAME) + + -- Calculate delta consumption energy + local delta_consumption_energy = 0.0 + if current_consumption_report ~= nil then + delta_consumption_energy = math.max((activeEnergyImportTotal * 1000) - current_consumption_report.energy, 0.0) + end + + -- Refresh Power Consumption Report + log.trace("refresh_usb_energy_meter(): Refreshing Power Consumption Report, dni = " .. dni) + device:emit_event(caps.powerConsumptionReport.powerConsumption({energy=activeEnergyImportTotal * 1000, deltaEnergy=delta_consumption_energy})) + + -- Save date of the last consumption + local current_energy_report = last_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL + if (current_energy_report + config.EDGE_CHILD_ENERGY_REPORT_INTERVAL) < os.time() then + current_energy_report = os.time() + end + + device:set_field(fields.LAST_ENERGY_REPORT, current_energy_report, {persist=false}) + else + log.debug("refresh_usb_energy_meter(): " .. config.EDGE_CHILD_ENERGY_REPORT_INTERVAL .. " seconds haven't elapsed yet! Last consumption was at " .. last_energy_report .. ", dni = " .. dni) + end + end + + return true +end + +function device_refresher.refresh_device(driver, device, values) + local dni, device_type = utils.get_dni_from_device(device) + log.info("device_refresher.refresh_device(): Refreshing data of device, dni = " .. dni) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + log.debug("device_refresher.refresh_device(): Cannot refresh bridge device, dni = " .. dni) + return + end + + log.debug("device_refresher.refresh_device(): Provided values: " .. utils.dump(values)) + + local refresh_methods = { + [utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE)] = refresh_current_sensor, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = refresh_energy_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = refresh_auxiliary_contact, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = refresh_output_module, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = refresh_energy_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = refresh_water_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = refresh_gas_meter, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = refresh_usb_energy_meter + } + + local device_model = utils.get_device_model(device) + if device_model == nil then + log.error("device_refresher.refresh_device(): No device model found for device, dni = " .. dni) + return + end + + local refresh_method = refresh_methods[device_model] + if refresh_method == nil then + log.error("device_refresher.refresh_device(): No refresh method found for device, dni = " .. dni .. ", model = " .. device_model) + return + end + + return refresh_method(driver, device, values) +end + +return device_refresher \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/commands.lua b/drivers/ABB/insite-scu200/src/commands.lua new file mode 100644 index 0000000000..453d0f521f --- /dev/null +++ b/drivers/ABB/insite-scu200/src/commands.lua @@ -0,0 +1,111 @@ +local caps = require('st.capabilities') +local log = require('log') +local json = require('dkjson') + +-- Local imports +local utils = require('utils') +local fields = require('fields') +local config = require("config") +local device_manager = require('abb.device_manager') +local connection_monitor = require('connection_monitor') + +-- Commands handler for the bridge and thing devices +local commands = {} + +-- Method for posting the payload to the device +local function post_payload(device, payload) + local dni = utils.get_dni_from_device(device) + local communication_device = device:get_parent_device() or device + local conn_info = communication_device:get_field(fields.CONN_INFO) + + local _, err, status = conn_info:post_device_by_id(dni, payload) + if not err and status == 200 then + log.info("post_payload(): Success, dni = " .. dni) + + return true + else + status = status or "nil" + log.error("post_payload(): Error, err = " .. err .. ", status = " .. status .. ", dni = " .. dni) + + device:offline() + + return false + end +end + +-- Switch on command +function commands.switch_on(driver, device, cmd) + local dni, _ = utils.get_dni_from_device(device) + log.info("commands.switch_on(): Switching on capablity within dni = " .. dni) + + local device_model = utils.get_device_model(device) + + local payload = nil + local event = nil + if device_model == utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE) then + payload = json.encode({capability = cmd.capability, command = cmd.command}) + event = caps.switch.switch.on() + end + + if payload ~= nil and event ~= nil then + local bridge = device:get_parent_device() + + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + local success = post_payload(device, payload) + if success then + device:emit_event(event) + end + end) + end +end + +-- Switch off commands +function commands.switch_off(driver, device, cmd) + local dni, _ = utils.get_dni_from_device(device) + log.info("commands.switch_off(): Switching off capablity within dni = " .. dni) + + local device_model = utils.get_device_model(device) + + local payload = nil + local event = nil + if device_model == utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE) then + payload = json.encode({capability = cmd.capability, command = cmd.command}) + event = caps.switch.switch.off() + end + + if payload ~= nil and event ~= nil then + local bridge = device:get_parent_device() + + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + local success = post_payload(device, payload) + if success then + device:emit_event(event) + end + end) + end +end + +-- Refresh command +function commands.refresh(driver, device, cmd) + local dni, device_type = utils.get_dni_from_device(device) + log.info("commands.refresh(): Refresh capability within dni = " .. dni) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + connection_monitor.check_and_update_connection(driver, device) + local child_devices = device:get_child_list() + + for _, thing_device in ipairs(child_devices) do + device_manager.refresh(driver, thing_device) + end + elseif device_type == fields.DEVICE_TYPE_THING then + local bridge = device:get_parent_device() + + if bridge.thread ~= nil then + bridge.thread:call_with_delay(0, function() -- Run within bridge thread to use the same connection + device_manager.refresh(driver, device) + end) + end + end +end + +return commands \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/config.lua b/drivers/ABB/insite-scu200/src/config.lua new file mode 100644 index 0000000000..8470708d9e --- /dev/null +++ b/drivers/ABB/insite-scu200/src/config.lua @@ -0,0 +1,72 @@ +local config = {} + +-- Device Config +config.DEVICE_TYPE = "LAN" + +config.MANUFACTURER = "ABB" + +config.BRIDGE_PROFILE = "abb.scu200.bridge.v1" +config.BRIDGE_TYPE = "SCU200" +config.BRIDGE_VERSION = "1" + +config.BRIDGE_URN = "urn:" .. config.MANUFACTURER .. ":device:" .. config.BRIDGE_TYPE .. ":" .. config.BRIDGE_VERSION + +config.BRIDGE_CONN_MONITOR_INTERVAL = 300 -- 5 minutes + +-- Edge Child Config +config.EDGE_CHILD_TYPE = "EDGE_CHILD" + +config.EDGE_CHILD_CURRENT_SENSOR_TYPE = "CurrentSensor" +config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE = "EnergyMeterModule" +config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE = "AuxiliaryContact" +config.EDGE_CHILD_OUTPUT_MODULE_TYPE = "OutputModule" +config.EDGE_CHILD_ENERGY_METER_TYPE = "EnergyMeter" +config.EDGE_CHILD_WATER_METER_TYPE = "WaterMeter" +config.EDGE_CHILD_GAS_METER_TYPE = "GasMeter" +config.EDGE_CHILD_USB_ENERGY_METER_TYPE = "USBEnergyMeter" + +config.EDGE_CHILD_CURRENT_SENSOR_VERSION = 1 +config.EDGE_CHILD_ENERGY_METER_MODULE_VERSION = 1 +config.EDGE_CHILD_AUXILIARY_CONTACT_VERSION = 1 +config.EDGE_CHILD_OUTPUT_MODULE_VERSION = 1 +config.EDGE_CHILD_ENERGY_METER_VERSION = 1 +config.EDGE_CHILD_WATER_METER_VERSION = 1 +config.EDGE_CHILD_GAS_METER_VERSION = 1 +config.EDGE_CHILD_USB_ENERGY_METER_VERSION = 1 + +config.EDGE_CHILD_CURRENT_SENSOR_CONSUMPTION_PROFILE = "abb.scu200.current-sensor-consumption.v1" +config.EDGE_CHILD_CURRENT_SENSOR_PRODUCTION_PROFILE = "abb.scu200.current-sensor-production.v1" +config.EDGE_CHILD_AUXILIARY_CONTACT_PROFILE = "abb.scu200.auxiliary-contact.v1" +config.EDGE_CHILD_OUTPUT_MODULE_PROFILE = "abb.scu200.output-module.v1" +config.EDGE_CHILD_ENERGY_METER_PROFILE = "abb.scu200.energy-meter.v1" +config.EDGE_CHILD_WATER_METER_PROFILE = "abb.scu200.water-meter.v1" +config.EDGE_CHILD_GAS_METER_PROFILE = "abb.scu200.gas-meter.v1" +config.EDGE_CHILD_USB_ENERGY_METER_PROFILE = "abb.scu200.usb-energy-meter.v1" + +config.EDGE_CHILD_CURRENT_SENSOR_REFRESH_PERIOD = 30 +config.EDGE_CHILD_ENERGY_METER_MODULE_REFRESH_PERIOD = 30 +config.EDGE_CHILD_AUXILIARY_CONTACT_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_OUTPUT_MODULE_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_ENERGY_METER_REFRESH_PERIOD = 30 +config.EDGE_CHILD_WATER_METER_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_GAS_METER_REFRESH_PERIOD = 300 -- 5 minutes +config.EDGE_CHILD_USB_ENERGY_METER_REFRESH_PERIOD = 30 + +config.EDGE_CHILD_ENERGY_REPORT_INTERVAL = 900 -- 15 minutes + +-- REST API Config +config.REST_API_PORT = 1025 + +-- SSDP Config +config.MC_ADDRESS = "239.255.255.250" +config.MC_PORT = 1900 +config.MC_TIMEOUT = 5 +config.MSEARCH = table.concat({ + "M-SEARCH * HTTP/1.1", + "HOST: 239.255.255.250:1900", + "MAN: \"ssdp:discover\"", + "MX: 5", + "ST: " .. config.BRIDGE_URN +}, "\r\n") + +return config diff --git a/drivers/ABB/insite-scu200/src/connection_monitor.lua b/drivers/ABB/insite-scu200/src/connection_monitor.lua new file mode 100644 index 0000000000..bdeb2b9621 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/connection_monitor.lua @@ -0,0 +1,71 @@ +local log = require("log") + +-- Local imports +local fields = require("fields") +local utils = require("utils") +local discovery = require("discovery") +local eventsource_handler = require("eventsource_handler") +local device_manager = require("abb.device_manager") + +-- Connection monitor for the SCU200 Bridge +local connection_monitor = {} + +function connection_monitor.update_connection(driver, device, bridge_ip) + local bridge_dni = utils.get_dni_from_device(device) + log.info("connection_monitor.update_connection(): Update connection for bridge device: " .. bridge_dni) + + local conn_info = device_manager.get_bridge_connection_info(driver, bridge_dni, bridge_ip) + + if device_manager.is_valid_connection(driver, device, conn_info) then + device:set_field(fields.CONN_INFO, conn_info) + eventsource_handler.create_sse(driver, device) + end +end + +local function find_new_connection(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("find_new_connection(): Find new connection for dni = " .. dni) + + local found_devices = discovery.find_devices() + + if found_devices ~= nil then + local found_device = found_devices[dni] + + if found_device then + log.info("find_new_connection(): Found new connection for dni = " .. dni) + + local ip = found_device.ip + + device:set_field(fields.BRIDGE_IPV4, ip, {persist = true}) + connection_monitor.update_connection(driver, device, ip) + end + end +end + +function connection_monitor.check_and_update_connection(driver, device) + local dni = utils.get_dni_from_device(device) + local conn_info = device:get_field(fields.CONN_INFO) + + if not device_manager.is_valid_connection(driver, device, conn_info) then + log.error("connection_monitor.check_and_update_connection(): Disconnected from device. Try to find new connection for dni = " .. dni) + + find_new_connection(driver, device) + end +end + +-- Method for monitoring the connection of the bridge devices +function connection_monitor.monitor_connections(driver) + local device_list = driver:get_devices() + + for _, device in ipairs(device_list) do + if device:get_field(fields.DEVICE_TYPE) == fields.DEVICE_TYPE_BRIDGE then + local dni = utils.get_dni_from_device(device) + log.info("connection_monitor.monitor_connections(): Monitoring connection for bridge device: " .. dni) + + connection_monitor.check_and_update_connection(driver, device) + device_manager.bridge_monitor(driver, device) + end + end +end + +return connection_monitor \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/discovery.lua b/drivers/ABB/insite-scu200/src/discovery.lua new file mode 100644 index 0000000000..3958c48026 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/discovery.lua @@ -0,0 +1,315 @@ +local log = require "log" +local socket = require('socket') +local cosock = require "cosock" + +-- Local imports +local api = require("abb.api") +local config = require("config") +local utils = require("utils") +local fields = require("fields") + +-- Discovery service run within SmartThings app +local discovery = {} + +local joined_bridge = {} +local joined_thing = {} + +-- Method for setting the device fields +function discovery.set_device_fields(driver, device) + local dni = utils.get_dni_from_device(device) + + if joined_bridge[dni] ~= nil then + log.info("discovery.set_device_field(): Setting device field for bridge: " .. dni) + local bridge_cache_value = driver.datastore.bridge_discovery_cache[dni] + + device:set_field(fields.BRIDGE_IPV4, bridge_cache_value.ip, {persist = true}) + device:set_field(fields.DEVICE_TYPE, fields.DEVICE_TYPE_BRIDGE, {persist = true}) + elseif joined_thing[dni] ~= nil then + log.info("discovery.set_device_field(): Setting device field for thing: " .. dni) + local thing_cache_value = driver.datastore.thing_discovery_cache[dni] + + device:set_field(fields.PARENT_BRIDGE_DNI, thing_cache_value.parent_bridge_dni, {persist = true}) + device:set_field(fields.THING_INFO, thing_cache_value.thing_info, {persist = true}) + device:set_field(fields.DEVICE_TYPE, fields.DEVICE_TYPE_THING, {persist = true}) + else + log.warn("discovery.set_device_field(): Could not set device field for unknown device: " .. dni) + end +end + +-- Method for updating the bridge discovery cache +local function update_bridge_discovery_cache(driver, dni, device) + log.info("update_bridge_discovery_cache(): Updating bridge discovery cache: " .. dni) + + driver.datastore.bridge_discovery_cache[dni] = { + ip = device["ip"] + } +end + +-- Method for updating the thing discovery cache +local function update_thing_discovery_cache(driver, thing_dni, parent_bridge_dni, thing_info) + log.info("update_thing_discovery_cache(): Updating thing discovery cache: " .. thing_dni) + + driver.datastore.thing_discovery_cache[thing_dni] = { + parent_bridge_dni = parent_bridge_dni, + thing_info = thing_info, + } +end + +-- Method for trying to add a new bridge +local function try_add_bridge(driver, dni, device) + log.info("try_add_bridge(): Trying to add bridge: " .. dni) + + local bridge_info = api.get_bridge_info(device["ip"], dni) + if bridge_info == nil then + log.error("try_add_bridge(): Failed to get bridge info for bridge: " .. dni) + return false + end + + update_bridge_discovery_cache(driver, dni, device) + + local metadata = { + type = config.DEVICE_TYPE, + device_network_id = dni, + label = bridge_info.name, + profile = config.BRIDGE_PROFILE, + manufacturer = config.MANUFACTURER, + model = config.BRIDGE_TYPE, + vendor_provided_label = config.BRIDGE_TYPE + } + + local success, err = driver:try_create_device(metadata) + + if success then + log.debug("try_add_bridge(): Bridge created: " .. dni) + + return true + else + log.error("try_add_bridge(): Failed to create bridge: " .. dni) + log.debug("try_add_bridge(): Error: " .. err) + + return false + end +end + +-- Method for trying to add a new thing +local function try_add_thing(driver, parent_device, thing_dni, thing_info) + local parent_device_dni = utils.get_dni_from_device(parent_device) + log.info("try_add_thing(): Trying to add thing: " .. thing_dni .. " of type: " .. thing_info.type .. " on bridge: " .. parent_device_dni) + + update_thing_discovery_cache(driver, thing_dni, parent_device_dni, thing_info) + + if thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE) or thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE) then + log.warn("try_add_thing(): Not supported thing type: " .. thing_info.type) + return false + elseif thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE) and thing_info.properties.isExport then + log.warn("try_add_thing(): Current sensor with production data is not supported") + return false + end + + local profile_ref = utils.get_thing_profile_ref(thing_info) + if profile_ref == nil then + log.error("try_add_thing(): Failed to get profile reference for thing: " .. thing_dni) + return false + end + + local parent_device_id = utils.get_device_id_from_device(parent_device) + + local metadata = { + type = config.EDGE_CHILD_TYPE, + label = thing_info.name, + vendor_provided_label = thing_info.name, + profile = profile_ref, + manufacturer = config.MANUFACTURER, + model = thing_info.type, + parent_device_id = parent_device_id, + parent_assigned_child_key = thing_info.uuid, + } + + local success, err = driver:try_create_device(metadata) + + if success then + log.debug("try_add_thing(): Thing created: " .. thing_dni) + + return true + else + log.error("try_add_thing(): Failed to create thing: " .. thing_dni) + log.debug("try_add_thing(): Error: " .. err) + + return false + end +end + +-- SSDP Response parser +local function parse_ssdp(data) + local res = {} + + res.status = data:sub(0, data:find('\r\n')) + + for line in data:gmatch("[^\r\n]+") do + local _, _, header, value = string.find(line, "([%w-]+):%s*([%a+-:_ /=?]*)") + + if header ~= nil and value ~= nil then + res[header:lower()] = value + end + end + + return res +end + +-- Method for finding devices +function discovery.find_devices() + log.info("discovery.find_devices(): Finding devices") + + -- Initialize UDP socket + local upnp = cosock.socket.udp() + + upnp:setsockname('*', 0) + upnp:setoption("broadcast", true) + upnp:settimeout(config.MC_TIMEOUT) + + -- Broadcast M-SEARCH request + log.info("discovery.find_devices(): Scanning network...") + + upnp:sendto(config.MSEARCH, config.MC_ADDRESS, config.MC_PORT) + + -- Listen for responses + local devices = {} + local start_time = socket.gettime() + + while (socket.gettime() - start_time) < config.MC_TIMEOUT do + local res = upnp:receivefrom() + + if res ~= nil then + local device = parse_ssdp(res) + local dni = string.match(device["usn"], "^uuid:([a-zA-Z0-9-]+)::" .. config.BRIDGE_URN .. "$") + + if dni ~= nil then + local _, _, device_ip = string.find(device["location"], "https?://(%d+%.%d+%.%d+%.%d+):?%d*/?.*") + device["ip"] = device_ip + + devices[dni] = device + end + end + end + + -- Print found devices + if next(devices) then + for dni, device in pairs(devices) do + log.debug("discovery.find_devices(): Device found: " .. utils.dump(device)) + end + else + log.debug("discovery.find_devices(): No devices found") + end + + -- Close the UDP socket + upnp:close() + + log.debug("discovery.find_devices(): Stop scanning network") + + if devices ~= nil then + return devices + end + + return nil +end + +-- Start the discovery of bridges +local function discover_bridges(driver) + log.info("discover_bridges(): Discovering bridges") + + -- Get the known devices + local known_devices = {} + + for _, device in pairs(driver:get_devices()) do + local dni, device_type = utils.get_dni_from_device(device) + known_devices[dni] = device + + log.debug("discover_bridges(): Known devices: " .. dni .. " with type: " .. device_type) + end + + -- Find new devices + local found_devices = discovery.find_devices() + + if found_devices ~= nil then + for dni, device in pairs(found_devices) do + if not known_devices or not known_devices[dni] then + log.info("discover_bridges(): Found new bridge: " .. dni) + + if not joined_bridge[dni] then + if try_add_bridge(driver, dni, device) then + joined_bridge[dni] = true + end + else + log.debug("discover_bridges(): Bridge already joined: " .. dni) + end + else + log.debug("discover_bridges(): Bridge already added: " .. dni) + end + end + end +end + +-- Start the discovery of things +local function discover_things(driver) + log.info("discover_things(): Discovering things") + + -- Get the known devices + local known_devices = {} + + for _, device in pairs(driver:get_devices()) do + local dni, device_type = utils.get_dni_from_device(device) + known_devices[dni] = device + + log.debug("discover_things(): Known devices: " .. dni .. " with type: " .. device_type) + end + + -- Found new devices + for bridge_dni, bridge_cache_value in pairs(driver.datastore.bridge_discovery_cache) do + local bridge_ip = bridge_cache_value.ip + log.info("discover_things(): Fetching things from bridge: " .. bridge_dni .. " at IP: " .. bridge_ip) + + if known_devices[bridge_dni] ~= nil and known_devices[bridge_dni]:get_field(fields.CONN_INFO) ~= nil then + local thing_infos = api.get_thing_infos(bridge_ip, bridge_dni) + + if thing_infos and thing_infos.devices ~= nil then + for _, thing_info in pairs(thing_infos.devices) do + if thing_info ~= nil then + local thing_dni = thing_info.uuid + + log.info("discover_things(): Found thing: " .. thing_dni .. " on bridge: " .. bridge_dni) + + if thing_dni ~= nil then + if not known_devices[thing_dni] then + if try_add_thing(driver, known_devices[bridge_dni], thing_dni, thing_info) then + joined_thing[thing_dni] = true + end + elseif not joined_thing[thing_dni] then + log.debug("discover_things(): Thing already known: " .. thing_dni) + else + log.debug("discover_things(): Thing already joined: " .. thing_dni) + end + end + end + + cosock.socket.sleep(0.2) + end + end + end + end +end + +-- Main function to start the discovery service +function discovery.start(driver, _, should_continue) + log.info("discovery.start(): Starting discovery") + + while should_continue() do + discover_bridges(driver) + discover_things(driver) + + cosock.socket.sleep(0.2) + end + + log.info("discovery.start(): Ending discovery") +end + +return discovery \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/eventsource_handler.lua b/drivers/ABB/insite-scu200/src/eventsource_handler.lua new file mode 100644 index 0000000000..82e7131b87 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/eventsource_handler.lua @@ -0,0 +1,128 @@ +local log = require('log') +local json = require('dkjson') + +-- Local imports +local EventSource = require "lunchbox.sse.eventsource" +local device_manager = require("abb.device_manager") +local device_refresher = require("abb.device_refresher") +local fields = require "fields" +local utils = require "utils" + +local eventsource_handler = {} + +-- Method for handling an incoming SSE event +function eventsource_handler.handle_sse_event(driver, bridge, msg) + log.debug("eventsource_handler.handle_sse_event(): Handling SSE event (TYPE: " .. msg.type .. ", DATA: " .. msg.data .. ")") + + if msg.type == "valueChanged" then + local data = json.decode(msg.data) + + if data ~= nil and next(data) ~= nil then + -- Find the device + local device = nil + + local child_devices = bridge:get_child_list() + for _, thing_device in ipairs(child_devices) do + local dni = utils.get_dni_from_device(thing_device) + + if dni == data.uuid then + device = thing_device + break + end + end + + if device == nil then + log.warn("eventsource_handler.handle_sse_event(): Failed to find the device with dni: " .. data.uuid) + return + end + + -- Prepare the values + local values = {} + + values[data.attribute.name] = data.attribute.value + + -- Refresh the device + if device_refresher.refresh_device(driver, device, values) then + -- Define online status + device:online() + else + log.error("eventsource_handler.handle_sse_event(): Failed to update the device's values") + + -- Set device as offline + device:offline() + end + else + log.error("eventsource_handler.handle_sse_event(): Failed to decode JSON data: " .. msg.data) + end + elseif msg.type == "noDevices" then + log.info("eventsource_handler.handle_sse_event(): No devices to monitor found") + + eventsource_handler.close_sse(driver, bridge) + elseif msg.type == "refreshConnection" then + log.info("eventsource_handler.handle_sse_event(): Refreshing connection") + + eventsource_handler.close_sse(driver, bridge) + eventsource_handler.create_sse(driver, bridge) + else + log.warn("eventsource_handler.handle_sse_event(): Unknown SSE event type: " .. msg.type) + end +end + +-- Method for creating SSE +function eventsource_handler.create_sse(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("eventsource_handler.create_sse(): Creating SSE for dni: " .. dni) + + local conn_info = device:get_field(fields.CONN_INFO) + + if not device_manager.is_valid_connection(driver, device, conn_info) then + log.error("eventsource_handler.create_sse(): Invalid connection for dni: " .. dni) + return + end + + local sse_url = conn_info:get_sse_url() + if not sse_url then + log.error("eventsource_handler.create_sse(): Failed to get sse_url for dni: " .. dni) + return + end + + log.trace("eventsource_handler.create_sse(): Creating SSE EventSource for " .. dni .. " with sse_url: " .. sse_url) + local eventsource = EventSource.new(sse_url, {}, nil, nil) + + eventsource.onmessage = function(msg) + if msg then + eventsource_handler.handle_sse_event(driver, device, msg) + end + end + + eventsource.onerror = function() + log.error("eventsource_handler.create_sse(): Error in the eventsource for dni: " .. dni) + device:offline() + end + + eventsource.onopen = function(msg) + log.info("eventsource_handler.create_sse(): Eventsource has been opened for dni: " .. dni) + device:online() + end + + local old_eventsource = device:get_field(fields.EVENT_SOURCE) + if old_eventsource then + log.info("eventsource_handler.create_sse(): Eventsource has been closed for dni: " .. dni) + old_eventsource:close() + end + device:set_field(fields.EVENT_SOURCE, eventsource) +end + +-- Method for closing SSE +function eventsource_handler.close_sse(driver, device) + local dni = utils.get_dni_from_device(device) + log.info("eventsource_handler.close_sse(): Closing SSE for dni: " .. dni) + + local eventsource = device:get_field(fields.EVENT_SOURCE) + if eventsource then + log.info("eventsource_handler.close_sse(): Closing eventsource for device: " .. dni) + eventsource:close() + end +end + +return eventsource_handler diff --git a/drivers/ABB/insite-scu200/src/fields.lua b/drivers/ABB/insite-scu200/src/fields.lua new file mode 100644 index 0000000000..7c56019cc1 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/fields.lua @@ -0,0 +1,16 @@ +-- Table of constants used to index in to device store fields +local fields = { + BRIDGE_IPV4 = "bridge_ipv4", + THING_INFO = "thing_info", + CONN_INFO = "conn_info", + PARENT_BRIDGE_DNI = "parent_bridge_dni", + EVENT_SOURCE = "eventsource", + DEVICE_TYPE = "devcie_type", + LAST_ENERGY_REPORT = "last_energy_report", + _INIT = "init", + + DEVICE_TYPE_BRIDGE = "bridge", + DEVICE_TYPE_THING = "thing" +} + +return fields \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/init.lua b/drivers/ABB/insite-scu200/src/init.lua new file mode 100644 index 0000000000..df33d18b1c --- /dev/null +++ b/drivers/ABB/insite-scu200/src/init.lua @@ -0,0 +1,45 @@ +local log = require('log') +local Driver = require('st.driver') +local caps = require('st.capabilities') + +-- Local imports +local discovery = require('discovery') +local commands = require('commands') +local config = require('config') +local lifecycles = require('lifecycles') +local connection_monitor = require('connection_monitor') + +-- Driver definition +local driver = Driver("ABB.SCU200", { + discovery = discovery.start, + lifecycle_handlers = lifecycles, + capability_handlers = { + -- Refresh command handler + [caps.refresh.ID] = { + [caps.refresh.commands.refresh.NAME] = commands.refresh + }, + [caps.switch.ID] = { + [caps.switch.commands.on.NAME] = commands.switch_on, + [caps.switch.commands.off.NAME] = commands.switch_off + } + } +}) + +-- Prepare datastores for bridge and thing discovery caches +if driver.datastore.bridge_discovery_cache == nil then + driver.datastore.bridge_discovery_cache = {} +end + +if driver.datastore.thing_discovery_cache == nil then + driver.datastore.thing_discovery_cache = {} +end + +-- Connection monitoring thread +driver:call_on_schedule(config.BRIDGE_CONN_MONITOR_INTERVAL, connection_monitor.monitor_connections, "SCU200 Bridge connection monitoring thread") + +-- Initialize driver +log.info("Starting driver") + +driver:run() + +log.warn("Exiting driver") \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lifecycles.lua b/drivers/ABB/insite-scu200/src/lifecycles.lua new file mode 100644 index 0000000000..83cdfa0eb7 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lifecycles.lua @@ -0,0 +1,87 @@ +local log = require('log') + +-- Local imports +local fields = require('fields') +local utils = require('utils') +local discovery = require('discovery') +local commands = require('commands') +local connection_monitor = require('connection_monitor') +local eventsource_handler = require("eventsource_handler") + +-- Lifecycles handlers for the driver +local lifecycles = {} + +-- Lifecycle handler for a device which has been initialized +function lifecycles.init(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + + -- Verify if the device has already been initialized + if device:get_field(fields._INIT) then + log.info("lifecycles.init(): Device already initialized: " .. dni .. " of type: " .. device_type) + return + end + + log.info("lifecycles.init(): Initializing device: " .. dni .. " of type: " .. device_type) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + if driver.datastore.bridge_discovery_cache[dni] then + log.debug("lifecycles.init(): Setting unsaved bridge fields") + discovery.set_device_fields(driver, device) + end + + local bridge_ip = device:get_field(fields.BRIDGE_IPV4) + + connection_monitor.update_connection(driver, device, bridge_ip) + elseif device_type == fields.DEVICE_TYPE_THING then + if driver.datastore.thing_discovery_cache[dni] then + log.debug("lifecycles.init(): Setting unsaved thing fields") + discovery.set_device_fields(driver, device) + end + + -- Refresh the device manually + commands.refresh(driver, device, nil) + + -- Refresh schedule + local refresh_period = utils.get_thing_refresh_period(device) + + device.thread:call_on_schedule( + refresh_period, + function () + return commands.refresh(driver, device, nil) + end, + "Refresh schedule") + end + + -- Set the device as initialized + device:set_field(fields._INIT, true, {persist = false}) +end + +-- Lifecycle handler for a device which has been added +function lifecycles.added(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + log.info("lifecycles.added(): Adding device: " .. dni .. " of type: " .. device_type) + + -- Force the initialization due to cases where the device is not initialized after being added + lifecycles.init(driver, device) +end + +-- Lifecycle handler for a device which has been removed +function lifecycles.removed(driver, device) + local dni, device_type = utils.get_dni_from_device(device) + log.info("lifecycles.removed(): Removing device: " .. dni .. " of type: " .. device_type) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + log.debug("lifecycles.removed(): Closing SSE for device: " .. dni) + + eventsource_handler.close_sse(driver, device) + elseif device_type == fields.DEVICE_TYPE_THING then + log.debug("lifecycles.removed(): Removing schedules for device: " .. dni) + + -- Remove the schedules to avoid unnecessary CPU processing + for timer in pairs(device.thread.timers) do + device.thread:cancel_timer(timer) + end + end +end + +return lifecycles \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/init.lua b/drivers/ABB/insite-scu200/src/lunchbox/init.lua new file mode 100644 index 0000000000..d454f39e68 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/init.lua @@ -0,0 +1,4 @@ +local RestClient = require "lunchbox.rest" +local EventSource = require "lunchbox.sse.eventsource" + +return {RestClient = RestClient, EventSource = EventSource} \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/rest.lua b/drivers/ABB/insite-scu200/src/lunchbox/rest.lua new file mode 100644 index 0000000000..b7c03525a6 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/rest.lua @@ -0,0 +1,427 @@ +---@class ChunkedResponse : Response +---@field package _received_body boolean +---@field package _parsed_headers boolean +---@field public new fun(status_code: number, socket: table?): ChunkedResponse +---@field public fill_body fun(self: ChunkedResponse): string? +---@field public append_body fun(self: ChunkedResponse, next_chunk_body: string): ChunkedResponse + +local socket = require "cosock.socket" +local utils = require "utils" +local lb_utils = require "lunchbox.util" +local Request = require "luncheon.request" +local Response = require "luncheon.response" --[[@as ChunkedResponse]] +local json = require('dkjson') + +local api_version = require("version").api + +local RestCallStates = { + SEND = "Send", + RECEIVE = "Receive", + RETRY = "Retry", + RECONNECT = "Reconnect", + COMPLETE = "Complete", +} + +local function connect(client) + local port = 80 + local use_ssl = false + + if client.base_url.scheme == "https" then + port = 443 + use_ssl = true + end + + if client.base_url.port ~= port then port = client.base_url.port end + local sock, err = client.socket_builder(client.base_url.host, port, use_ssl) + + if sock == nil then + client.socket = nil + return false, err + end + + client.socket = sock + return true +end + +local function reconnect(client) + if client.socket ~= nil then + client.socket:close() + client.socket = nil + end + return connect(client) +end + +---comment +---@param client RestClient +---@param request Request +---@return integer? bytes_sent +---@return string? err_msg +---@return integer idx +local function send_request(client, request) + if client.socket == nil then + return nil, "no socket available", 0 + end + local payload = request:serialize() + + local bytes, err, idx = nil, nil, 0 + + repeat bytes, err, idx = client.socket:send(payload, idx + 1, #payload) until (bytes == #payload) + or (err ~= nil) + + return bytes, err, idx +end + +local function parse_chunked_response(original_response, sock) + local ChunkedTransferStates = { + EXPECTING_CHUNK_LENGTH = "ExpectingChunkLength", + EXPECTING_BODY_CHUNK = "ExpectingBodyChunk", + } + + local full_response = Response.new(original_response.status, nil) --[[@as ChunkedResponse]] + + for header in original_response.headers:iter() do full_response.headers:append_chunk(header) end + + local original_body, err = original_response:get_body() + if type(original_body) ~= "string" or err ~= nil then + return original_body, (err or "unexpected nil in error position") + end + local next_chunk_bytes = tonumber(original_body, 16) + local next_chunk_body = "" + local bytes_read = 0; + + local state = ChunkedTransferStates.EXPECTING_BODY_CHUNK + + repeat + local pat = nil + local next_recv, next_err, partial = nil, nil, nil + + if state == ChunkedTransferStates.EXPECTING_BODY_CHUNK then + pat = next_chunk_bytes + else + pat = "*l" + end + + next_recv, next_err, partial = sock:receive(pat) + + if next_err ~= nil then + if string.lower(next_err) == "closed" then + if partial ~= nil and #partial >= 1 then + full_response:append_body(partial) + next_chunk_bytes = 0 + else + return nil, next_err + end + else + return nil, ("unexpected error reading chunked transfer: " .. next_err) + end + end + + if next_recv ~= nil and #next_recv >= 1 then + if state == ChunkedTransferStates.EXPECTING_BODY_CHUNK then + bytes_read = bytes_read + #next_recv + next_chunk_body = next_chunk_body .. next_recv + + if bytes_read >= next_chunk_bytes then + full_response = full_response:append_body(next_chunk_body) + next_chunk_body = "" + bytes_read = 0 + + state = ChunkedTransferStates.EXPECTING_CHUNK_LENGTH + end + elseif state == ChunkedTransferStates.EXPECTING_CHUNK_LENGTH then + next_chunk_bytes = tonumber(next_recv, 16) + + state = ChunkedTransferStates.EXPECTING_BODY_CHUNK + end + end + until next_chunk_bytes == 0 + + local _ = sock:receive("*l") -- clear the trailing CRLF + + full_response._received_body = true + full_response._parsed_headers = true + + return full_response +end + +local function recv_additional_response(original_response, sock) + local full_response = Response.new(original_response.status, nil) + local headers = original_response:get_headers() + local content_length_str = headers:get_one("Content-Length") + local content_length = nil + local bytes_read = 0 + if content_length_str then + content_length = math.tointeger(content_length_str) + end + + local next_recv, next_err, partial + + repeat + next_recv, next_err, partial = sock:receive(content_length - bytes_read) + + if next_recv ~= nil and #next_recv >= 1 then + full_response:append_body(next_recv) + bytes_read = bytes_read + #next_recv + end + + if partial ~= nil and #partial >= 1 then + full_response:append_body(partial) + bytes_read = bytes_read + #partial + end + until next_err == "closed" or bytes_read >= content_length + + full_response._received_body = true + full_response._parsed_headers = true + + return full_response +end + +local function handle_response(sock) + if api_version >= 9 then + local response, err = Response.tcp_source(sock) + if err or (not response) then return response, (err or "unknown error") end + return response, response:fill_body() + end + -- called select right before passing in so we receive immediately + local initial_recv, initial_err, partial = Response.source(function() return sock:receive('*l') end) + + local full_response = nil + + if initial_recv ~= nil then + local headers = initial_recv:get_headers() + + if headers:get_one("Content-Length") then + full_response = recv_additional_response(initial_recv, sock) + elseif headers and headers:get_one("Transfer-Encoding") == "chunked" then + local response, err = parse_chunked_response(initial_recv, sock) + if err ~= nil then + return nil, err + end + full_response = response + else + full_response = initial_recv + end + + return full_response + else + return nil, initial_err, partial + end +end + +local function execute_request(client, request, retry_fn) + if not client._active then + return nil, "Called `execute request` on a terminated REST Client", nil + end + + if client.socket == nil then + local success, err = connect(client) + if not success then return nil, err, nil end + end + + local should_retry = retry_fn + + if type(should_retry) ~= "function" then + should_retry = function() return false end + end + + -- send output + local _bytes_sent, send_err, _idx = nil, nil, 0 + -- recv output + local response, recv_err, partial = nil, nil, nil + -- return values + local ret, err = nil, nil + + local backoff = utils.backoff_builder(60, 1, 0.1) + local current_state = RestCallStates.SEND + + repeat + local retry = should_retry() + if current_state == RestCallStates.SEND then + backoff = utils.backoff_builder(60, 1, 0.1) + _bytes_sent, send_err, _idx = send_request(client, request) + + if not send_err then + current_state = RestCallStates.RECEIVE + elseif retry then + if string.lower(send_err) == "closed" or string.lower(send_err):match("broken pipe") then + current_state = RestCallStates.RECONNECT + else + current_state = RestCallStates.RETRY + end + else + ret = nil + err = send_err + current_state = RestCallStates.COMPLETE + end + elseif current_state == RestCallStates.RECEIVE then + response, recv_err, partial = handle_response(client.socket) + + if not recv_err then + ret = response + err = nil + current_state = RestCallStates.COMPLETE + elseif retry then + if string.lower(recv_err) == "closed" or string.lower(recv_err):match("broken pipe") then + current_state = RestCallStates.RECONNECT + else + current_state = RestCallStates.RETRY + end + else + ret = nil + err = recv_err + current_state = RestCallStates.COMPLETE + end + elseif current_state == RestCallStates.RECONNECT then + local success, reconn_err = reconnect(client) + if success then + current_state = RestCallStates.RETRY + elseif not retry then + ret = nil + err = reconn_err + current_state = RestCallStates.COMPLETE + else + socket.sleep(backoff()) + end + elseif current_state == RestCallStates.RETRY then + bytes_sent, send_err, _idx = nil, nil, 0 + response, recv_err, partial = nil, nil, nil + current_state = RestCallStates.SEND + socket.sleep(backoff()) + end + until current_state == RestCallStates.COMPLETE + + return ret, err, partial +end + +---@class RestClient +--- +---@field base_url table `net.url` URL table +---@field socket table `cosock` TCP socket +local RestClient = {} +RestClient.__index = RestClient + +function RestClient.one_shot_get(full_url, additional_headers, socket_builder) + local url_table = lb_utils.force_url_table(full_url) + + local query_params = "" + if url_table.query ~= nil then + query_params = "?" + + for param, value in pairs(url_table.query) do + query_params = query_params .. param .. "=" .. value .. "&" + end + + query_params = query_params:sub(1, -2) + end + + local client = RestClient.new(url_table.scheme .. "://" .. url_table.authority, socket_builder) + local ret, err = client:get(url_table.path .. query_params, additional_headers) + + client:shutdown() + + return ret, err +end + +function RestClient.one_shot_post(full_url, body, additional_headers, socket_builder) + local url_table = lb_utils.force_url_table(full_url) + + local query_params = "" + if url_table.query ~= nil then + query_params = "?" + + for param, value in pairs(url_table.query) do + query_params = query_params .. param .. "=" .. value .. "&" + end + + query_params = query_params:sub(1, -2) + end + + if type(body) == "table" then + body = json.encode(body) + end + + local client = RestClient.new(url_table.scheme .. "://" .. url_table.authority, socket_builder) + local ret, err = client:post(url_table.path .. query_params, body, additional_headers) + + client:shutdown() + + return ret, err +end + +function RestClient:close_socket() + if self.socket ~= nil and self._active then + self.socket:close() + self.socket = nil + end +end + +function RestClient:shutdown() + self:close_socket() + self._active = false +end + +function RestClient:update_base_url(new_url) + if self.socket ~= nil then + self.socket:close() + self.socket = nil + end + + self.base_url = lb_utils.force_url_table(new_url) +end + +function RestClient:get(path, additional_headers, retry_fn) + local request = Request.new("GET", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + return execute_request(self, request, retry_fn) +end + +function RestClient:post(path, body_string, additional_headers, retry_fn) + local request = Request.new("POST", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + request = request:append_body(body_string) + + return execute_request(self, request, retry_fn) +end + +function RestClient:put(path, body_string, additional_headers, retry_fn) + local request = Request.new("PUT", path, nil):add_header( + "user-agent", "smartthings-lua-edge-driver" + ):add_header("host", string.format("%s", self.base_url.host)):add_header( + "connection", "keep-alive" + ) + + if additional_headers ~= nil and type(additional_headers) == "table" then + for k, v in pairs(additional_headers) do request = request:add_header(k, v) end + end + + request = request:append_body(body_string) + + return execute_request(self, request, retry_fn) +end + +function RestClient.new(base_url, sock_builder) + base_url = lb_utils.force_url_table(base_url) + + if type(sock_builder) ~= "function" then sock_builder = utils.labeled_socket_builder() end + + return + setmetatable({base_url = base_url, socket_builder = sock_builder, socket = nil, _active = true}, RestClient) +end + +return RestClient \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua b/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua new file mode 100644 index 0000000000..d9a5930df2 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/sse/eventsource.lua @@ -0,0 +1,523 @@ +local cosock = require "cosock" +local socket = require "cosock.socket" +local ssl = require "cosock.ssl" +local json = require "dkjson" + +local log = require "log" +local util = require "lunchbox.util" +local Request = require "luncheon.request" +local Response = require "luncheon.response" + +--- A pure Lua implementation of the EventSource interface. +--- The EventSource interface represents the client end of an HTTP(S) +--- connection that receives an event stream following the Server-Sent events +--- specification. +--- +--- MDN Documentation for EventSource: https://developer.mozilla.org/en-US/docs/Web/API/EventSource +--- HTML Spec: https://html.spec.whatwg.org/multipage/server-sent-events.html +--- +--- @class EventSource +--- @field public url table A `net.url` table representing the URL for the connection +--- @field public ready_state number Enumeration of the ready states outlined in the spec. +--- @field public onopen function in-line callback for on-open events +--- @field public onmessage function in-line callback for on-message events +--- @field public onerror function in-line callback for on-error events; error callbacks will fire +--- @field package _reconnect boolean flag that says whether or not the client should attempt to reconnect on close. +--- @field package _reconnect_time_millis number The amount of time to wait between reconnects, in millis. Can be sent by the server. +--- @field package _sock_builder function|nil optional. If this function exists, it will be called to create a new TCP socket on connection. +--- @field package _sock table? the TCP socket for the connection +--- @field package _needs_more boolean flag to track whether or not we're still expecting mroe on this source before we dispatch +--- @field package _last_field string the last field the parsing path saw, in case it needs to append more to its value +--- @field package _extra_headers table a table of string:string key-value pairs that will be inserted in to the initial requests's headers. +--- @field package _parse_buffers table inner state, keeps track of the various event stream buffers in between dispatches. +--- @field package _listeners table event listeners attached using the add_event_listener API instead of the inline callbacks. +local EventSource = {} +EventSource.__index = EventSource + +--- The Ready States that an EventSource can be in. We use base 0 to match the specification. +EventSource.ReadyStates = util.read_only { + CONNECTING = 0, -- The connection has not yet been established + OPEN = 1, -- The connection is open + CLOSED = 2 -- The connection has closed +} + +--- The event types supported by this source, patterned after their values in JavaScript. +EventSource.EventTypes = util.read_only { + ON_OPEN = "open", + ON_MESSAGE = "message", + ON_ERROR = "error", +} + +--- Helper function that creates the initial Request to start the stream. +--- @function create_request +--- @local +--- @param url_table table a net.url table +--- @param extra_headers table a set of key/value pairs (strings) to capture any extra HTTP headers needed +--- @param body table a set of key/value pairs (strings) to be sent as the body of the initial POST request. +local function create_request(url_table, extra_headers, body) + local request = Request.new("POST", url_table.path, nil) + :add_header("user-agent", "smartthings-lua-edge-driver") + :add_header("host", string.format("%s", url_table.host)) + :add_header("connection", "keep-alive") + :add_header("accept", "text/event-stream") + :add_header("content-type", "application/json") + + if type(extra_headers) == "table" then + for k, v in pairs(extra_headers) do + request = request:add_header(k, v) + end + end + + local encoded_body = json.encode(body) + request = request:append_body(encoded_body) + + return request +end + +--- Helper function to send the request and kick off the stream. +--- @function send_stream_start_request +--- @local +--- @param payload string the entire string buffer to send +--- @param sock table the TCP socket to send it over +local function send_stream_start_request(payload, sock) + local bytes, err, idx = nil, nil, 0 + + repeat + bytes, err, idx = sock:send(payload, idx + 1, #payload) + until (bytes == #payload) or (err ~= nil) + + if err then + log.error_with({ hub_logs = true }, "send error: " .. err) + end + + return bytes, err, idx +end + +--- Helper function to create an table representing an event from the source's parse buffers. +--- @function make_event +--- @local +--- @param source EventSource +local function make_event(source) + local event_type = nil + + if #source._parse_buffers["event"] > 0 then + event_type = source._parse_buffers["event"] + end + + return { + type = event_type or "message", + data = source._parse_buffers["data"], + origin = source.url.scheme .. "://" .. source.url.host, + lastEventId = source._parse_buffers["id"] + } +end + +--- SSE spec for dispatching an event: +--- https://html.spec.whatwg.org/multipage/server-sent-events.html#dispatchMessage +--- @function dispatch_event +--- @local +--- @param source EventSource +local function dispatch_event(source) + local data_buffer = source._parse_buffers["data"] + local is_blank_line = data_buffer ~= nil and + (#data_buffer == 0) or + data_buffer == "\n" or + data_buffer == "\r" or + data_buffer == "\r\n" + if data_buffer ~= nil and not is_blank_line then + local event = util.read_only(make_event(source)) + + if type(source.onmessage) == "function" then + source.onmessage(event) + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_MESSAGE]) do + if type(listener) == "function" then + listener(event) + end + end + end + + source._parse_buffers["event"] = "" + source._parse_buffers["data"] = "" +end + +local valid_fields = util.read_only { + ["event"] = true, + ["data"] = true, + ["id"] = true, + ["retry"] = true +} + +-- An event stream "line" can end in more than one way; from the spec: +-- Lines must be separated by either +-- a U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair, +-- a single U+000A LINE FEED (LF) character, +-- or a single U+000D CARRIAGE RETURN (CR) character. +-- +-- util.iter_string_lines won't suffice here because: +-- a.) it assumes \n, and +-- b.) it doesn't differentiate between a "line" that ends without a newline and one that does. +-- +-- h/t to github.com/FreeMasen for the suggestions on the efficient implementation of this +local function find_line_endings(chunk) + local r_idx, n_idx = string.find(chunk, "[\r\n]+") + if r_idx == nil or r_idx == n_idx then + -- 1 character or no match + return r_idx, n_idx + end + local slice = string.sub(chunk, r_idx, n_idx) + if slice == "\r\n" then + return r_idx, n_idx + end + -- invalid multi character match, return first character only + return r_idx, r_idx +end + +local function event_lines(chunk) + local remaining = chunk + local line_end, rn_end + local remainder_sent = false + return function() + line_end, rn_end = find_line_endings(remaining) + if not line_end then + if remainder_sent or (not remaining) or #remaining == 0 then + return nil + else + remainder_sent = true + return remaining, false + end + end + local next_line = string.sub(remaining, 1, line_end - 1) + remaining = string.sub(remaining, rn_end + 1) + return next_line, true + end +end +--- SSE spec for interpreting an event stream: +--- https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface +--- @function parse +--- @local +--- @param source EventSource +--- @param recv string the received payload from the last socket receive +local function sse_parse_chunk(source, recv) + for line, complete in event_lines(recv) do + if not source._needs_more and (#line == 0 or (not line:match("([%w%p]+)"))) then -- empty/blank lines indicate dispatch + dispatch_event(source) + elseif source._needs_more then + local append = line + if source._last_field == "data" and complete then append = append .. "\n" end + if complete then source._needs_more = false end + source._parse_buffers[source._last_field] = source._parse_buffers[source._last_field] .. append + else + if line:sub(1, 1) ~= ":" then -- ignore any complete lines that start w/ a colon + local matches = line:gmatch("(%w*)(:*)(.*)") -- colon after field is optional, in that case it's a field w/ no value + + for field, _colon, value in matches do + value = value:gsub("^[^%g]", "", 1) -- trim a single leading space character + + if valid_fields[field] then + source._last_field = field + if field == "retry" then + local new_time = tonumber(value, 10) + if type(new_time) == "number" then + source._reconnect_time_millis = new_time + end + elseif field == "data" then + local append = (value or "") + -- if complete then append = append .. "\n" end + source._parse_buffers[field] = source._parse_buffers[field] .. append + elseif field == "id" then + -- skip ID's if they contain the NULL character + if not string.find(value, '\0') then + source._parse_buffers[field] = value + end + else + source._parse_buffers[field] = value + end + end + source._needs_more = source._needs_more or (not complete) + end + end + end + end +end + +--- Helper function that captures the cyclic logic of the EventSource while in the CONNECTING state. +--- @function connecting_action +--- @local +--- @param source EventSource +local function connecting_action(source) + if not source._sock then + if type(source._sock_builder) == "function" then + source._sock = source._sock_builder() + else + source._sock, err = socket.tcp() + if err ~= nil then return nil, err end + + _, err = source._sock:settimeout(60) + if err ~= nil then return nil, err end + + _, err = source._sock:connect(source.url.host, source.url.port) + if err ~= nil then return nil, err end + + _, err = source._sock:setoption("keepalive", true) + if err ~= nil then return nil, err end + + if source.url.scheme == "https" then + source._sock, err = ssl.wrap(source._sock, { + mode = "client", + protocol = "any", + verify = "none", + options = "all" + }) + if err ~= nil then return nil, err end + + _, err = source._sock:dohandshake() + if err ~= nil then return nil, err end + end + end + end + + local request = create_request(source.url, source._extra_headers, source._body) + + local last_event_id = source._parse_buffers["id"] + + if last_event_id ~= nil and #last_event_id > 0 then + request = request:add_header("Last-Event-ID", last_event_id) + end + + local _, err, _ = send_stream_start_request(request:serialize(), source._sock) + + if err ~= nil then + return nil, err + end + + local response + response, err = Response.tcp_source(source._sock) + + if not response or err ~= nil then + return nil, err or "nil response from Response.tcp_source" + end + + if response.status ~= 200 then + return nil, "Server responded with status other than 200 OK", { response.status, response.status_msg } + end + + local headers, err = response:get_headers() + if err ~= nil then + return nil, err + end + local content_type = string.lower((headers and headers:get_one('content-type') or "none")) + if not content_type:find("text/event-stream", 1, true) then + local err_msg = "Expected content type of text/event-stream in response headers, received: " .. content_type + return nil, err_msg + end + + source.ready_state = EventSource.ReadyStates.OPEN + + if type(source.onopen) == "function" then + source.onopen() + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_OPEN]) do + if type(listener) == "function" then + listener() + end + end +end +--- Helper function that captures the cyclic logic of the EventSource while in the OPEN state. +--- @function open_action +--- @local +--- @param source EventSource +local function open_action(source) + local recv, err, partial = source._sock:receive('*l') + + if err then + --- connection is fine but there was nothing + --- to be read from the other end so we just + --- early return. + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + + -- the number of bytes to read per the chunked encoding spec + local recv_as_num = tonumber(recv, 16) + + if recv_as_num ~= nil and recv_as_num == 0 then + return -- the stream has ended + end + + 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. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + + local _, err, partial = source._sock:receive('*l') -- clear the final line + + if err then + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + source._sock:close() + source._sock = nil + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial + end + end + sse_parse_chunk(source, recv) + else + local recv_dbg = recv or "" + if #recv_dbg == 0 then + recv_dbg = "" + end + recv_dbg = recv_dbg:gsub("\r\n", ""):gsub("\n", ""):gsub("\r", "") + log.error_with({ hub_logs = true }, string.format("Received %s while expecting a chunked encoding payload length (hex number)\n", recv_dbg)) + end +end + +--- Helper function that captures the cyclic logic of the EventSource while in the CLOSED state. +--- @function closed_action +--- @local +--- @param source EventSource +local function closed_action(source) + if source._sock ~= nil then + source._sock:close() + source._sock = nil + end + + if source._reconnect then + if type(source.onerror) == "function" then + source.onerror() + end + + for _, listener in ipairs(source._listeners[EventSource.EventTypes.ON_ERROR]) do + if type(listener) == "function" then + listener() + end + end + + local sleep_time_secs = source._reconnect_time_millis / 1000.0 + socket.sleep(sleep_time_secs) + + source.ready_state = EventSource.ReadyStates.CONNECTING + end +end + +local state_actions = { + [EventSource.ReadyStates.CONNECTING] = connecting_action, + [EventSource.ReadyStates.OPEN] = open_action, + [EventSource.ReadyStates.CLOSED] = closed_action +} + +--- Create a new EventSource. The only required parameter is the URL, which can +--- be a string or a net.url table. The string form will be converted to a net.url table. +--- +--- @param url string|table a string or a net.url table representing the complete URL (minimally a scheme/host/path, port optional) for the event stream. +--- @param extra_headers table|nil an optional table of key-value pairs (strings) to be added to the initial POST request +--- @param body table|nil an optional table of key-value pairs (strings) to be sent as the body of the initial POST request +--- @param sock_builder function|nil an optional function to be used to create the TCP socket for the stream. If nil, a set of defaults will be used to create a new TCP socket. +--- @return EventSource a new EventSource +function EventSource.new(url, extra_headers, body, sock_builder) + local url_table = util.force_url_table(url) + + if not url_table.port then + if url_table.scheme == "http" then + url_table.port = 80 + elseif url_table.scheme == "https" then + url_table.port = 443 + end + end + + local sock = nil + + if type(sock_builder) == "function" then + sock = sock_builder() + end + + local source = setmetatable({ + url = url_table, + ready_state = EventSource.ReadyStates.CONNECTING, + onopen = nil, + onmessage = nil, + onerror = nil, + _needs_more = false, + _last_field = nil, + _reconnect = true, + _reconnect_time_millis = 15 * 1000, + _sock_builder = sock_builder, + _sock = sock, + _extra_headers = extra_headers, + _body = body, + _parse_buffers = { + ["data"] = "", + ["id"] = "", + ["event"] = "", + }, + _listeners = { + [EventSource.EventTypes.ON_OPEN] = {}, + [EventSource.EventTypes.ON_MESSAGE] = {}, + [EventSource.EventTypes.ON_ERROR] = {} + }, + }, EventSource) + + cosock.spawn(function() + local st_utils = require "st.utils" + while true do + if source.ready_state == EventSource.ReadyStates.CLOSED and not source._reconnect then + return + end + local _, action_err, partial = state_actions[source.ready_state](source) + if action_err ~= nil then + if action_err ~= "timeout" or action_err ~= "wantread" then + log.error_with({ hub_logs = true }, "Event Source Coroutine State Machine error: " .. action_err) + if partial ~= nil and #partial > 0 then + log.error_with({ hub_logs = true }, st_utils.stringify_table(partial, "\tReceived Partial", true)) + end + source.ready_state = EventSource.ReadyStates.CLOSED + end + end + end + end) + + return source +end + +--- Close the event source, signalling that a reconnect is not desired +function EventSource:close() + self._reconnect = false + if self._sock ~= nil then + self._sock:close() + end + self._sock = nil + self.ready_state = EventSource.ReadyStates.CLOSED +end + +--- Add a callback to the event source +---@param listener_type string One of "message", "open", or "error" +---@param listener function the callback to be called in case of an event. Open and Error events have no payload. The message event will have a single argument, a table. +function EventSource:add_event_listener(listener_type, listener) + local list = self._listeners[listener_type] + + if list then + table.insert(list, listener) + end +end + +return EventSource \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/lunchbox/util.lua b/drivers/ABB/insite-scu200/src/lunchbox/util.lua new file mode 100644 index 0000000000..6eb94407eb --- /dev/null +++ b/drivers/ABB/insite-scu200/src/lunchbox/util.lua @@ -0,0 +1,46 @@ +local net_url = require "net.url" + +local util = {} + +util.force_url_table = function(url) + if type(url) ~= "table" then url = net_url.parse(url) end + + if not url.port then + if url.scheme == "http" then + url.port = 80 + elseif url.scheme == "https" then + url.port = 443 + end + end + + return url +end + +util.read_only = function(tbl) + if type(tbl) == "table" then + local proxy = {} + local mt = { -- create metatable + __index = tbl, + __newindex = function(t, k, v) error("attempt to update a read-only table", 2) end, + } + setmetatable(proxy, mt) + return proxy + else + return tbl + end +end + +util.iter_string_lines = function(str) + if str:sub(-1) ~= "\n" then str = str .. "\n" end + + return str:gmatch("(.-)\n") +end + +util.copy_data = function(tbl) + local ret = {} + for k, v in pairs(tbl) do ret[k] = v end + + return ret +end + +return util \ No newline at end of file diff --git a/drivers/ABB/insite-scu200/src/utils.lua b/drivers/ABB/insite-scu200/src/utils.lua new file mode 100644 index 0000000000..139b5503c5 --- /dev/null +++ b/drivers/ABB/insite-scu200/src/utils.lua @@ -0,0 +1,219 @@ +local log = require("log") +local socket = require "cosock.socket" +local ssl = require "cosock.ssl" + +-- Local imports +local config = require("config") +local fields = require("fields") + +-- Utility functions for the SmartThings edge driver +local utils = {} + +-- Get the device id from the device +function utils.get_dni_from_device(device) + if device.parent_assigned_child_key then + local thing_dni = device.parent_assigned_child_key + + return thing_dni, fields.DEVICE_TYPE_THING + else + local bridge_dni = device.device_network_id + + return bridge_dni, fields.DEVICE_TYPE_BRIDGE + end +end + +-- Get the device ID +function utils.get_device_id_from_device(device) + return device.st_store.id +end + +-- Get the device model +function utils.get_device_model(device) + local thing_info = device:get_field(fields.THING_INFO) + + if thing_info == nil then + return nil + end + + return thing_info.type +end + +-- Get the device IP address +function utils.get_device_ip_address(device) + local _, device_type = utils.get_dni_from_device(device) + + if device_type == fields.DEVICE_TYPE_BRIDGE then + return device:get_field(fields.BRIDGE_IPV4) + else + local bridge = device:get_parent_device() + + return bridge:get_field(fields.BRIDGE_IPV4) + end +end + +-- Method for getting edge child device version by type +function utils.get_edge_child_device_version(edge_child_device_type) + local edge_child_device_versions = { + [config.EDGE_CHILD_CURRENT_SENSOR_TYPE] = config.EDGE_CHILD_CURRENT_SENSOR_VERSION, + [config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE] = config.EDGE_CHILD_ENERGY_METER_MODULE_VERSION, + [config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE] = config.EDGE_CHILD_AUXILIARY_CONTACT_VERSION, + [config.EDGE_CHILD_OUTPUT_MODULE_TYPE] = config.EDGE_CHILD_OUTPUT_MODULE_VERSION, + [config.EDGE_CHILD_ENERGY_METER_TYPE] = config.EDGE_CHILD_ENERGY_METER_VERSION, + [config.EDGE_CHILD_WATER_METER_TYPE] = config.EDGE_CHILD_WATER_METER_VERSION, + [config.EDGE_CHILD_GAS_METER_TYPE] = config.EDGE_CHILD_GAS_METER_VERSION, + [config.EDGE_CHILD_USB_ENERGY_METER_TYPE] = config.EDGE_CHILD_USB_ENERGY_METER_VERSION + } + + return edge_child_device_versions[edge_child_device_type] +end + +-- Method for getting the thing exact type +function utils.get_thing_exact_type(edge_child_device_type) + local device_version = utils.get_edge_child_device_version(edge_child_device_type) + + if device_version == nil then + return nil + end + + return config.MANUFACTURER .. "_" .. config.BRIDGE_TYPE .. "_" .. edge_child_device_type .. "_" .. device_version +end + +-- Method for getting the thing profile reference +function utils.get_thing_profile_ref(thing_info) + if thing_info.type == utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE) then + if thing_info.properties.isExport then + return config.EDGE_CHILD_CURRENT_SENSOR_PRODUCTION_PROFILE + else + return config.EDGE_CHILD_CURRENT_SENSOR_CONSUMPTION_PROFILE + end + end + + local thing_profiles = { + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = config.EDGE_CHILD_ENERGY_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = config.EDGE_CHILD_AUXILIARY_CONTACT_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = config.EDGE_CHILD_OUTPUT_MODULE_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = config.EDGE_CHILD_ENERGY_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = config.EDGE_CHILD_WATER_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = config.EDGE_CHILD_GAS_METER_PROFILE, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = config.EDGE_CHILD_USB_ENERGY_METER_PROFILE + } + + return thing_profiles[thing_info.type] +end + +-- Method for getting the thing refresh period +function utils.get_thing_refresh_period(device) + local device_model = utils.get_device_model(device) + + local thing_refresh_periods = { + [utils.get_thing_exact_type(config.EDGE_CHILD_CURRENT_SENSOR_TYPE)] = config.EDGE_CHILD_CURRENT_SENSOR_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_MODULE_TYPE)] = config.EDGE_CHILD_ENERGY_METER_MODULE_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_AUXILIARY_CONTACT_TYPE)] = config.EDGE_CHILD_AUXILIARY_CONTACT_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_OUTPUT_MODULE_TYPE)] = config.EDGE_CHILD_OUTPUT_MODULE_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_ENERGY_METER_TYPE)] = config.EDGE_CHILD_ENERGY_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_WATER_METER_TYPE)] = config.EDGE_CHILD_WATER_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_GAS_METER_TYPE)] = config.EDGE_CHILD_GAS_METER_REFRESH_PERIOD, + [utils.get_thing_exact_type(config.EDGE_CHILD_USB_ENERGY_METER_TYPE)] = config.EDGE_CHILD_USB_ENERGY_METER_REFRESH_PERIOD + } + + return thing_refresh_periods[device_model] +end + +-- Method for dumping a table to string +function utils.dump(o) + if type(o) == "table" then + local s = '{' + + for k,v in pairs(o) do + if type(k) ~= "number" then k = '"'..k..'"' end + s = s .. ' ['..k..'] = ' .. utils.dump(v) .. ',' + end + + return s .. '} ' + else + return tostring(o) + end +end + +-- Method for building a exponential backoff time value generator +function utils.backoff_builder(max, inc, rand) + local count = 0 + inc = inc or 1 + + return function() + local randval = 0 + if rand then + randval = math.random() * rand * 2 - rand + end + + local base = inc * (2 ^ count - 1) + count = count + 1 + + -- ensure base backoff (not including random factor) is less than max + if max then base = math.min(base, max) end + + -- ensure total backoff is >= 0 + return math.max(base + randval, 0) + end +end + +-- Method for creating a labeled socket +function utils.labeled_socket_builder(label, ssl_config) + label = (label or "") + if #label > 0 then + label = label .. " " + end + + if not ssl_config then + ssl_config = { mode = "client", protocol = "any", verify = "none", options = "all" } + end + + local function make_socket(host, port, wrap_ssl) + log.info("utils.labeled_socket_builder(): Creating TCP socket for REST Connection: " .. label) + local _ = nil + local sock, err = socket.tcp() + + if err ~= nil or (not sock) then + return nil, (err or "unknown error creating TCP socket") + end + + log.debug("utils.labeled_socket_builder(): Setting TCP socket timeout for REST Connection: " .. label) + _, err = sock:settimeout(60) + if err ~= nil then + return nil, "settimeout error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Connecting TCP socket for REST Connection: " .. label) + _, err = sock:connect(host, port) + if err ~= nil then + return nil, "Connect error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Set Keepalive for TCP socket for REST Connection: " .. label) + _, err = sock:setoption("keepalive", true) + if err ~= nil then + return nil, "Setoption error: " .. err + end + + if wrap_ssl then + log.debug("utils.labeled_socket_builder(): Creating SSL wrapper for REST Connection: " .. label) + sock, err = ssl.wrap(sock, ssl_config) + if err ~= nil then + return nil, "SSL wrap error: " .. err + end + + log.debug("utils.labeled_socket_builder(): Performing SSL handshake for REST Connection: " .. label) + _, err = sock:dohandshake() + if err ~= nil then + return nil, "Error with SSL handshake: " .. err + end + end + + log.info("utils.labeled_socket_builder(): Successfully created TCP connection: " .. label) + return sock, err + end + + return make_socket +end + +return utils \ No newline at end of file From f2aa768ca9d2400b5df17948fefdd7d7a26c3fa0 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:17 -0600 Subject: [PATCH 418/449] CHAD-17063: Zigbee-button lazy load subdrivers --- .../zigbee-button/src/aqara/can_handle.lua | 12 +++ .../zigbee-button/src/aqara/fingerprints.lua | 13 +++ .../zigbee-button/src/aqara/init.lua | 45 +++-------- .../zigbee-button/src/button_utils.lua | 15 +--- .../src/dimming-remote/can_handle.lua | 14 ++++ .../src/dimming-remote/fingerprints.lua | 9 +++ .../zigbee-button/src/dimming-remote/init.lua | 30 +------ .../zigbee-button/src/ewelink/can_handle.lua | 14 ++++ .../src/ewelink/fingerprints.lua | 9 +++ .../zigbee-button/src/ewelink/init.lua | 32 ++------ .../zigbee-button/src/ezviz/can_handle.lua | 18 +++++ .../zigbee-button/src/ezviz/init.lua | 29 ++----- .../zigbee-button/src/frient/can_handle.lua | 11 +++ .../zigbee-button/src/frient/init.lua | 20 +---- .../SmartThings/zigbee-button/src/init.lua | 29 +------ .../zigbee-button/src/iris/can_handle.lua | 14 ++++ .../zigbee-button/src/iris/fingerprints.lua | 9 +++ .../zigbee-button/src/iris/init.lua | 30 +------ .../zigbee-button/src/lazy_load_subdriver.lua | 15 ++++ .../src/pushButton/can_handle.lua | 11 +++ .../zigbee-button/src/pushButton/init.lua | 30 +------ .../zigbee-button/src/samjin/can_handle.lua | 11 +++ .../zigbee-button/src/samjin/init.lua | 20 +---- .../zigbee-button/src/st/zigbee/zdo/init.lua | 17 +--- .../zigbee-button/src/sub_drivers.lua | 17 ++++ .../src/test/test_SLED_button.lua | 15 +--- .../src/test/test_aduro_button.lua | 15 +--- .../src/test/test_aqara_button.lua | 15 +--- .../src/test/test_centralite_button.lua | 15 +--- .../src/test/test_dimming_remote.lua | 17 +--- .../src/test/test_ewelink_button.lua | 17 +--- .../src/test/test_ezviz_button.lua | 17 +--- .../src/test/test_frient_button.lua | 15 +--- .../src/test/test_heiman_button.lua | 15 +--- .../src/test/test_ikea_on_off.lua | 15 +--- .../src/test/test_ikea_open_close.lua | 15 +--- .../src/test/test_ikea_remote_control.lua | 17 +--- .../src/test/test_iris_button.lua | 15 +--- .../test/test_linxura_aura_smart_button.lua | 17 +--- ...est_linxura_smart_controller_4x_button.lua | 17 +--- .../src/test/test_push_only_button.lua | 15 +--- .../src/test/test_robb_4x_button.lua | 15 +--- .../src/test/test_robb_8x_button.lua | 15 +--- .../src/test/test_samjin_button.lua | 15 +--- .../src/test/test_shinasystem_button.lua | 15 +--- .../src/test/test_somfy_situo_1_button.lua | 15 +--- .../src/test/test_somfy_situo_4_button.lua | 15 +--- .../src/test/test_thirdreality_button.lua | 3 + .../src/test/test_vimar_button.lua | 15 +--- .../src/test/test_wallhero_button.lua | 15 +--- .../src/test/test_zigbee_button.lua | 15 +--- .../src/test/test_zigbee_ecosmart_button.lua | 15 +--- .../src/test/test_zunzunbee_8_button.lua | 17 +--- .../src/thirdreality/can_handle.lua | 11 +++ .../zigbee-button/src/thirdreality/init.lua | 7 +- .../zigbee-multi-button/SLED/can_handle.lua | 11 +++ .../src/zigbee-multi-button/SLED/init.lua | 20 +---- .../adurosmart/can_handle.lua | 14 ++++ .../adurosmart/fingerprints.lua | 11 +++ .../zigbee-multi-button/adurosmart/init.lua | 32 +------- .../src/zigbee-multi-button/can_handle.lua | 14 ++++ .../centralite/can_handle.lua | 14 ++++ .../centralite/fingerprints.lua | 9 +++ .../zigbee-multi-button/centralite/init.lua | 30 +------ .../ecosmart/can_handle.lua | 11 +++ .../src/zigbee-multi-button/ecosmart/init.lua | 20 +---- .../src/zigbee-multi-button/fingerprints.lua | 42 ++++++++++ .../zigbee-multi-button/heiman/can_handle.lua | 14 ++++ .../heiman/fingerprints.lua | 10 +++ .../src/zigbee-multi-button/heiman/init.lua | 37 ++------- .../ikea/TRADFRI_on_off_switch/can_handle.lua | 9 +++ .../init.lua} | 19 +---- .../ikea/TRADFRI_open_close_remote.lua | 36 --------- .../TRADFRI_open_close_remote/can_handle.lua | 9 +++ .../ikea/TRADFRI_open_close_remote/init.lua | 23 ++++++ .../TRADFRI_remote_control/can_handle.lua | 9 +++ .../init.lua} | 19 +---- .../zigbee-multi-button/ikea/can_handle.lua | 14 ++++ .../zigbee-multi-button/ikea/fingerprints.lua | 10 +++ .../src/zigbee-multi-button/ikea/init.lua | 37 ++------- .../zigbee-multi-button/ikea/sub_drivers.lua | 12 +++ .../src/zigbee-multi-button/init.lua | 80 ++----------------- .../linxura/can_handle.lua | 14 ++++ .../linxura/fingerprints.lua | 9 +++ .../src/zigbee-multi-button/linxura/init.lua | 30 +------ .../zigbee-multi-button/robb/can_handle.lua | 15 ++++ .../zigbee-multi-button/robb/fingerprints.lua | 15 ++++ .../src/zigbee-multi-button/robb/init.lua | 13 +-- .../shinasystems/can_handle.lua | 14 ++++ .../shinasystems/fingerprints.lua | 17 ++++ .../zigbee-multi-button/shinasystems/init.lua | 41 ++-------- .../zigbee-multi-button/somfy/can_handle.lua | 11 +++ .../src/zigbee-multi-button/somfy/init.lua | 25 ++---- .../somfy/somfy_situo_1/can_handle.lua | 9 +++ .../init.lua} | 19 +---- .../somfy/somfy_situo_4/can_handle.lua | 9 +++ .../init.lua} | 19 +---- .../zigbee-multi-button/somfy/sub_drivers.lua | 11 +++ .../src/zigbee-multi-button/sub_drivers.lua | 20 +++++ .../zigbee-multi-button/supported_values.lua | 15 +--- .../zigbee-multi-button/vimar/can_handle.lua | 11 +++ .../src/zigbee-multi-button/vimar/init.lua | 20 +---- .../wallhero/can_handle.lua | 14 ++++ .../wallhero/fingerprints.lua | 8 ++ .../src/zigbee-multi-button/wallhero/init.lua | 29 +------ .../zunzunbee/can_handle.lua | 15 ++++ .../zunzunbee/fingerprints.lua | 9 +++ .../zigbee-multi-button/zunzunbee/init.lua | 33 +------- 108 files changed, 821 insertions(+), 1104 deletions(-) create mode 100644 drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/frient/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/iris/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua rename drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/{TRADFRI_on_off_switch.lua => TRADFRI_on_off_switch/init.lua} (55%) delete mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua rename drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/{TRADFRI_remote_control.lua => TRADFRI_remote_control/init.lua} (81%) create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua rename drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/{somfy_situo_1.lua => somfy_situo_1/init.lua} (78%) create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua rename drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/{somfy_situo_4.lua => somfy_situo_4/init.lua} (83%) create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua new file mode 100644 index 0000000000..3dc96661cd --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/aqara/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aqara_products = function(opts, driver, device) + local FINGERPRINTS = require "aqara.fingerprints" + if FINGERPRINTS[device:get_model()] and FINGERPRINTS[device:get_model()].mfr == device:get_manufacturer() then + return true, require("aqara") + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..83f7c77050 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/aqara/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Mini Switch T1 + ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch E1 (Single Rocker) + ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Single Rocker) + ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Double Rocker) + ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2450", quantity = 1 }, -- Aqara Wireless Remote Switch H1 (Single Rocker) + ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2450", quantity = 1 } -- Aqara Wireless Remote Switch H1 (Double Rocker) +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 1fb762cbbc..7467fcfe42 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local battery_defaults = require "st.zigbee.defaults.battery_defaults" local clusters = require "st.zigbee.zcl.clusters" @@ -34,14 +24,7 @@ local MULTISTATE_INPUT_CLUSTER_ID = 0x0012 local PRESENT_ATTRIBUTE_ID = 0x0055 local COMP_LIST = { "button1", "button2", "all" } -local AQARA_REMOTE_SWITCH = { - ["lumi.remote.b1acn02"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Mini Switch T1 - ["lumi.remote.acn003"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch E1 (Single Rocker) - ["lumi.remote.b186acn03"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Single Rocker) - ["lumi.remote.b286acn03"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2032", quantity = 1 }, -- Aqara Wireless Remote Switch T1 (Double Rocker) - ["lumi.remote.b18ac1"] = { mfr = "LUMI", btn_cnt = 1, type = "CR2450", quantity = 1 }, -- Aqara Wireless Remote Switch H1 (Single Rocker) - ["lumi.remote.b28ac1"] = { mfr = "LUMI", btn_cnt = 3, type = "CR2450", quantity = 1 } -- Aqara Wireless Remote Switch H1 (Double Rocker) -} +local FINGERPRINTS = require "aqara.fingerprints" local configuration = { { @@ -65,7 +48,7 @@ local configuration = { local function present_value_attr_handler(driver, device, value, zb_rx) if value.value < 0xFF then local end_point = zb_rx.address_header.src_endpoint.value - local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 local evt = capabilities.button.button.held({ state_change = true }) if value.value == 1 then evt = capabilities.button.button.pushed({ state_change = true }) @@ -110,7 +93,7 @@ local function battery_level_handler(driver, device, value, zb_rx) end local function mode_switching_handler(driver, device, value, zb_rx) - local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 local allow = device.preferences[MODE_CHANGE] or false if allow then local mode = device:get_field(MODE) or 1 @@ -139,14 +122,6 @@ local function mode_switching_handler(driver, device, value, zb_rx) end end -local is_aqara_products = function(opts, driver, device) - local isAqaraProducts = false - if AQARA_REMOTE_SWITCH[device:get_model()] and AQARA_REMOTE_SWITCH[device:get_model()].mfr == device:get_manufacturer() then - isAqaraProducts = true - end - return isAqaraProducts -end - local function device_init(driver, device) battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) if configuration ~= nil then @@ -157,11 +132,11 @@ local function device_init(driver, device) end local function added_handler(self, device) - local btn_evt_cnt = AQARA_REMOTE_SWITCH[device:get_model()].btn_cnt or 1 + local btn_evt_cnt = FINGERPRINTS[device:get_model()].btn_cnt or 1 local mode = device:get_field(MODE) or 0 local model = device:get_model() - local type = AQARA_REMOTE_SWITCH[device:get_model()].type or "CR2032" - local quantity = AQARA_REMOTE_SWITCH[device:get_model()].quantity or 1 + local type = FINGERPRINTS[device:get_model()].type or "CR2032" + local quantity = FINGERPRINTS[device:get_model()].quantity or 1 if mode == 0 then if model == "lumi.remote.b18ac1" or model == "lumi.remote.b28ac1" then @@ -233,7 +208,7 @@ local aqara_wireless_switch_handler = { } } }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } return aqara_wireless_switch_handler diff --git a/drivers/SmartThings/zigbee-button/src/button_utils.lua b/drivers/SmartThings/zigbee-button/src/button_utils.lua index 4a8f7101e0..10ca4ab11a 100644 --- a/drivers/SmartThings/zigbee-button/src/button_utils.lua +++ b/drivers/SmartThings/zigbee-button/src/button_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local log = require "log" diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua new file mode 100644 index 0000000000..d3a469e214 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_dimming_remote(opts, driver, device, ...) + local FINGERPRINTS = require("dimming-remote.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("dimming-remote") + end + end + return false +end + +return can_handle_zigbee_dimming_remote diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua new file mode 100644 index 0000000000..439694ebaf --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIBEE_DIMMING_SWITCH_FINGERPRINTS = { + { mfr = "OSRAM", model = "LIGHTIFY Dimming Switch" }, + { mfr = "CentraLite", model = "3130" } +} + +return ZIBEE_DIMMING_SWITCH_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua index d08c975632..54ca675ccd 100644 --- a/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua +++ b/drivers/SmartThings/zigbee-button/src/dimming-remote/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -22,19 +12,7 @@ local battery_defaults = require "st.zigbee.defaults.battery_defaults" local button_utils = require "button_utils" -local ZIBEE_DIMMING_SWITCH_FINGERPRINTS = { - { mfr = "OSRAM", model = "LIGHTIFY Dimming Switch" }, - { mfr = "CentraLite", model = "3130" } -} -local function can_handle_zigbee_dimming_remote(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIBEE_DIMMING_SWITCH_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function button_pushed_handler(button_number) return function(self, device, value, zb_rx) @@ -84,7 +62,7 @@ local dimming_remote = { } } }, - can_handle = can_handle_zigbee_dimming_remote + can_handle = require("dimming-remote.can_handle"), } return dimming_remote diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua b/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua new file mode 100644 index 0000000000..6a9426b75d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ewelink/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ewelink_button(opts, driver, device, ...) + local FINGERPRINTS = require("ewelink.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("ewelink") + end + end + return false +end + +return can_handle_ewelink_button diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua new file mode 100644 index 0000000000..3e21d092d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ewelink/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local EWELINK_BUTTON_FINGERPRINTS = { + { mfr = "eWeLink", model = "WB01" }, + { mfr = "eWeLink", model = "SNZB-01P" } +} + +return EWELINK_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/ewelink/init.lua b/drivers/SmartThings/zigbee-button/src/ewelink/init.lua index 334dd79108..b503b68c64 100644 --- a/drivers/SmartThings/zigbee-button/src/ewelink/init.lua +++ b/drivers/SmartThings/zigbee-button/src/ewelink/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -19,19 +9,7 @@ local device_management = require "st.zigbee.device_management" local OnOff = clusters.OnOff local button = capabilities.button.button -local EWELINK_BUTTON_FINGERPRINTS = { - { mfr = "eWeLink", model = "WB01" }, - { mfr = "eWeLink", model = "SNZB-01P" } -} -local function can_handle_ewelink_button(opts, driver, device, ...) - for _, fingerprint in ipairs(EWELINK_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function do_configure(driver, device) device:configure() @@ -61,7 +39,7 @@ local ewelink_button = { } } }, - can_handle = can_handle_ewelink_button + can_handle = require("ewelink.can_handle"), } -return ewelink_button \ No newline at end of file +return ewelink_button diff --git a/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua b/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua new file mode 100644 index 0000000000..b046b8966c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/ezviz/can_handle.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_ezviz_button = function(opts, driver, device) + + local EZVIZ_PRIVATE_BUTTON_CLUSTER = 0xFE05 + local EZVIZ_PRIVATE_STANDARD_CLUSTER = 0xFE00 + local EZVIZ_MFR = "EZVIZ" + + local support_button_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_BUTTON_CLUSTER) + local support_standard_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_STANDARD_CLUSTER) + if device:get_manufacturer() == EZVIZ_MFR and support_button_cluster and support_standard_cluster then + return true, require("ezviz") + end + return false +end + +return is_ezviz_button diff --git a/drivers/SmartThings/zigbee-button/src/ezviz/init.lua b/drivers/SmartThings/zigbee-button/src/ezviz/init.lua index 5b630843a2..5bea38ffb4 100644 --- a/drivers/SmartThings/zigbee-button/src/ezviz/init.lua +++ b/drivers/SmartThings/zigbee-button/src/ezviz/init.lua @@ -1,31 +1,12 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local EZVIZ_PRIVATE_BUTTON_CLUSTER = 0xFE05 -local EZVIZ_PRIVATE_STANDARD_CLUSTER = 0xFE00 local EZVIZ_PRIVATE_BUTTON_ATTRIBUTE = 0x0000 -local EZVIZ_MFR = "EZVIZ" -local is_ezviz_button = function(opts, driver, device) - local support_button_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_BUTTON_CLUSTER) - local support_standard_cluster = device:supports_server_cluster(EZVIZ_PRIVATE_STANDARD_CLUSTER) - if device:get_manufacturer() == EZVIZ_MFR and support_button_cluster and support_standard_cluster then - return true - end -end local ezviz_private_cluster_button_handler = function(driver, device, zb_rx) local event @@ -53,6 +34,6 @@ local ezviz_button_handler = { } } }, - can_handle = is_ezviz_button + can_handle = require("ezviz.can_handle"), } -return ezviz_button_handler \ No newline at end of file +return ezviz_button_handler diff --git a/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua b/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua new file mode 100644 index 0000000000..924c595fd3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/frient/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function frient_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "frient A/S" and (device:get_model() == "SBTZB-110" or device:get_model() == "MBTZB-110") then + return true, require("frient") + end + return false +end + +return frient_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/frient/init.lua b/drivers/SmartThings/zigbee-button/src/frient/init.lua index 17b963548d..bf622cd694 100644 --- a/drivers/SmartThings/zigbee-button/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-button/src/frient/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -232,8 +222,6 @@ local frient_button = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "frient A/S" and (device:get_model() == "SBTZB-110" or device:get_model() == "MBTZB-110") - end + can_handle = require("frient.can_handle"), } return frient_button diff --git a/drivers/SmartThings/zigbee-button/src/init.lua b/drivers/SmartThings/zigbee-button/src/init.lua index a61ff415a7..8ed0db27db 100644 --- a/drivers/SmartThings/zigbee-button/src/init.lua +++ b/drivers/SmartThings/zigbee-button/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -140,18 +130,7 @@ local zigbee_button_driver_template = { } } }, - sub_drivers = { - require("aqara"), - require("pushButton"), - require("frient"), - require("zigbee-multi-button"), - require("dimming-remote"), - require("iris"), - require("samjin"), - require("ewelink"), - require("thirdreality"), - require("ezviz") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { added = added_handler, }, diff --git a/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua b/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua new file mode 100644 index 0000000000..618bc6eb5a --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/iris/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_iris_button(opts, driver, device, ...) + local FINGERPRINTS = require("iris.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("iris") + end + end + return false +end + +return can_handle_iris_button diff --git a/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua new file mode 100644 index 0000000000..4ae1761e06 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/iris/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IRIS_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3455-L" }, + { mfr = "CentraLite", model = "3460-L" } +} + +return IRIS_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/iris/init.lua b/drivers/SmartThings/zigbee-button/src/iris/init.lua index 157e766748..a6eac49870 100644 --- a/drivers/SmartThings/zigbee-button/src/iris/init.lua +++ b/drivers/SmartThings/zigbee-button/src/iris/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -21,19 +11,7 @@ local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local button_utils = require "button_utils" -local IRIS_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3455-L" }, - { mfr = "CentraLite", model = "3460-L" } -} -local function can_handle_iris_button(opts, driver, device, ...) - for _, fingerprint in ipairs(IRIS_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function button_pressed_handler(self, device, value, zb_rx) button_utils.init_button_press(device) @@ -94,7 +72,7 @@ local iris_button = { } } }, - can_handle = can_handle_iris_button + can_handle = require("iris.can_handle"), } return iris_button diff --git a/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua b/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua new file mode 100644 index 0000000000..f25592fb01 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/pushButton/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function pushButton_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "HEIMAN" and device:get_model() == "SOS-EM" then + return true, require("pushButton") + end + return false +end + +return pushButton_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua index 6fec59d69e..170307a600 100644 --- a/drivers/SmartThings/zigbee-button/src/pushButton/init.lua +++ b/drivers/SmartThings/zigbee-button/src/pushButton/init.lua @@ -1,27 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- Copyright 2020 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 button_utils = require "button_utils" @@ -38,9 +18,7 @@ local push_button = { added = added_handler, }, sub_drivers = {}, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "HEIMAN" and device:get_model() == "SOS-EM" - end + can_handle = require("pushButton.can_handle"), } return push_button diff --git a/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua b/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua new file mode 100644 index 0000000000..0886bedb9c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/samjin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samjin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samjin" and device:get_model() == "button" then + return true, require("samjin") + end + return false +end + +return samjin_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/samjin/init.lua b/drivers/SmartThings/zigbee-button/src/samjin/init.lua index 116e6aaa80..bd254091a6 100644 --- a/drivers/SmartThings/zigbee-button/src/samjin/init.lua +++ b/drivers/SmartThings/zigbee-button/src/samjin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -28,9 +18,7 @@ local samjin_button = { lifecycle_handlers = { init = init_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samjin" and device:get_model() == "button" - end + can_handle = require("samjin.can_handle"), } return samjin_button diff --git a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua index 9d009d47cb..3d832ac180 100644 --- a/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua +++ b/drivers/SmartThings/zigbee-button/src/st/zigbee/zdo/init.lua @@ -1,16 +1,5 @@ --- Copyright 2021 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. +-- Copyright 2021 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local data_types = require "st.zigbee.data_types" local utils = require "st.zigbee.utils" local zdo_commands = require "st.zigbee.zdo.commands" @@ -152,4 +141,4 @@ end setmetatable(zdo_messages.ZdoMessageBody, { __call = zdo_messages.ZdoMessageBody.from_values }) -return zdo_messages \ No newline at end of file +return zdo_messages diff --git a/drivers/SmartThings/zigbee-button/src/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/sub_drivers.lua new file mode 100644 index 0000000000..47fe5ff9c4 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/sub_drivers.lua @@ -0,0 +1,17 @@ +-- 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("aqara"), + lazy_load_if_possible("pushButton"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("zigbee-multi-button"), + lazy_load_if_possible("dimming-remote"), + lazy_load_if_possible("iris"), + lazy_load_if_possible("samjin"), + lazy_load_if_possible("ewelink"), + lazy_load_if_possible("thirdreality"), + lazy_load_if_possible("ezviz"), +} +return sub_drivers 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 fecbeeabb7..cfbd4a6845 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" 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 2736ea3d96..27b8da764b 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 838b9545fb..67314ef651 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 6c2e46f724..c8d5ff87ae 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 4ba87cb461..c552322c9b 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -272,4 +261,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 d08e38391a..5562eced88 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -151,4 +140,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 77b6768149..be613bbc14 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -196,4 +185,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 3df15b9f08..aa6f211d65 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 f0604a0cd8..0cc2c734cf 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 9479358793..82f477f426 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 7b41684c5a..3d5c7ab58d 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 6b16ed842c..ef08a1d7e5 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" @@ -305,4 +294,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 f28e47dece..64630415a0 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 4b9ca6dfd9..8e3ff6e001 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -119,4 +108,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 a53d5f3851..27ec0a8e44 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 @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -119,4 +108,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 ed046c5781..4af8ef8c3c 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 554decefc1..4888ae5f7e 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 67554e92a0..ebb324a3dc 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 cf3bd7fe3d..1a23d68d83 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 6f677e80b7..5278edbf8e 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local base64 = require "st.base64" 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 83e9fec30d..7e7e155867 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 c0329f5561..d358db3fcc 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local capabilities = require "st.capabilities" 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 bdb8e51bf6..a102e30e51 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" local data_types = require "st.zigbee.data_types" 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 e1d35fb6bc..575175aa47 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 e62cbd7055..98d8efbdfd 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" 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 97feca9121..92a50636a4 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 8f956e9203..c6f28dfe44 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 a92e1c6285..ae579dd671 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 @@ -1,16 +1,5 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -203,4 +192,4 @@ test.register_coroutine_test( ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua b/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua new file mode 100644 index 0000000000..77d97bb6a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/thirdreality/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function thirdreality_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RSB22BZ" then + return true, require("thirdreality") + end + return false +end + +return thirdreality_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua b/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua index c4ce10f1bf..f56745c0c1 100644 --- a/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua +++ b/drivers/SmartThings/zigbee-button/src/thirdreality/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local MULTISTATE_INPUT_ATTR = 0x0012 @@ -41,9 +44,7 @@ local thirdreality_device_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Third Reality, Inc" and device:get_model() == "3RSB22BZ" - end + can_handle = require("thirdreality.can_handle"), } return thirdreality_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua new file mode 100644 index 0000000000..4e96689e08 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function SLED_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Samsung Electronics" and device:get_model() == "SAMSUNG-ITM-Z-005" then + return true, require("zigbee-multi-button.SLED") + end + return false +end + +return SLED_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua index ea6a9d5faa..cab7458507 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/SLED/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -72,9 +62,7 @@ local SLED_button = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Samsung Electronics" and device:get_model() == "SAMSUNG-ITM-Z-005" - end + can_handle = require("zigbee-multi-button.SLED.can_handle"), } return SLED_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua new file mode 100644 index 0000000000..9b4eedd1ff --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_aduro_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.adurosmart.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.adurosmart") + end + end + return false +end + +return is_aduro_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua new file mode 100644 index 0000000000..c5870be270 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ADURO_BUTTON_FINGERPRINTS = { + { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, + { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, + { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, + { mfr = "ADUROLIGHT", model = "Adurolight_NCC" } +} + +return ADURO_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua index 76a6ba6aae..56a29a1d0a 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/adurosmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -25,21 +15,7 @@ local ADURO_NUM_ENDPOINT = 0x04 local ADURO_MANUFACTURER_SPECIFIC_CLUSTER = 0xFCCC local ADURO_MANUFACTURER_SPECIFIC_CMD = 0x00 -local ADURO_BUTTON_FINGERPRINTS = { - { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, - { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, - { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, - { mfr = "ADUROLIGHT", model = "Adurolight_NCC" } -} -local is_aduro_button = function(opts, driver, device) - for _, fingerprint in ipairs(ADURO_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configuration = function(self, device) for endpoint = 1,ADURO_NUM_ENDPOINT do @@ -83,7 +59,7 @@ local aduro_device_handler = { } } }, - can_handle = is_aduro_button + can_handle = require("zigbee-multi-button.adurosmart.can_handle"), } return aduro_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua new file mode 100644 index 0000000000..bd1c57940f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zigbee_multi_button(opts, driver, device, ...) + local FINGERPRINTS = require("zigbee-multi-button.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button") + end + end + return false +end + +return can_handle_zigbee_multi_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua new file mode 100644 index 0000000000..0216d3729c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_centralite_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.centralite.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.centralite") + end + end + return false +end + +return is_centralite_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua new file mode 100644 index 0000000000..d742cb6c64 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local CENTRALITE_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3450-L" }, + { mfr = "CentraLite", model = "3450-L2" } +} + +return CENTRALITE_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua index 8eb3762be4..d291efb0cb 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/centralite/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local device_management = require "st.zigbee.device_management" @@ -30,19 +20,7 @@ local EP_BUTTON_COMPONENT_MAP = { [0x04] = 2 } -local CENTRALITE_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3450-L" }, - { mfr = "CentraLite", model = "3450-L2" } -} -local is_centralite_button = function(opts, driver, device) - for _, fingerprint in ipairs(CENTRALITE_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local do_configuration = function(self, device) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) @@ -75,7 +53,7 @@ local centralite_device_handler = { } } }, - can_handle = is_centralite_button + can_handle = require("zigbee-multi-button.centralite.can_handle"), } return centralite_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua new file mode 100644 index 0000000000..217df6b26f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ecosmart_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "LDS" and device:get_model() == "ZBT-CCTSwitch-D0001" then + return true, require("zigbee-multi-button.ecosmart") + end + return false +end + +return ecosmart_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua index 8f7d4ec6d8..215adba69c 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ecosmart/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -126,9 +116,7 @@ local ecosmart_button = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "LDS" and device:get_model() == "ZBT-CCTSwitch-D0001" - end + can_handle = require("zigbee-multi-button.ecosmart.can_handle"), } return ecosmart_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua new file mode 100644 index 0000000000..cf3903152d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua @@ -0,0 +1,42 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { + { mfr = "CentraLite", model = "3450-L" }, + { mfr = "CentraLite", model = "3450-L2" }, + { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, + { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, + { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, + { mfr = "ADUROLIGHT", model = "Adurolight_NCC" }, + { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0" }, + { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0" }, + { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0" }, + { mfr = "IKEA of Sweden", model = "TRADFRI on/off switch" }, + { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, + { mfr = "IKEA of Sweden", model = "TRADFRI remote control" }, + { mfr = "KE", model = "TRADFRI open/close remote" }, + { mfr = "\x02KE", model = "TRADFRI open/close remote" }, + { mfr = "SOMFY", model = "Situo 1 Zigbee" }, + { mfr = "SOMFY", model = "Situo 4 Zigbee" }, + { mfr = "LDS", model = "ZBT-CCTSwitch-D0001" }, + { mfr = "ShinaSystem", model = "MSM-300Z" }, + { mfr = "ShinaSystem", model = "BSM-300Z" }, + { mfr = "ShinaSystem", model = "SBM300ZB1" }, + { mfr = "ShinaSystem", model = "SBM300ZB2" }, + { mfr = "ShinaSystem", model = "SBM300ZB3" }, + { mfr = "ShinaSystem", model = "SBM300ZC1" }, + { mfr = "ShinaSystem", model = "SBM300ZC2" }, + { mfr = "ShinaSystem", model = "SBM300ZC3" }, + { mfr = "ShinaSystem", model = "SBM300ZC4" }, + { mfr = "ShinaSystem", model = "SQM300ZC4" }, + { mfr = "ROBB smarrt", model = "ROB_200-007-0" }, + { mfr = "ROBB smarrt", model = "ROB_200-008-0" }, + { mfr = "WALL HERO", model = "ACL-401SCA4" }, + { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-005" }, + { mfr = "Vimar", model = "RemoteControl_v1.0" }, + { mfr = "Linxura", model = "Smart Controller" }, + { mfr = "Linxura", model = "Aura Smart Button" }, + { mfr = "zunzunbee", model = "SSWZ8T" } +} + +return ZIGBEE_MULTI_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua new file mode 100644 index 0000000000..09e0575266 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_heiman_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.heiman.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.heiman") + end + end + return false +end + +return is_heiman_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua new file mode 100644 index 0000000000..68723ba700 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local HEIMAN_BUTTON_FINGERPRINTS = { + { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0", endpoint_num = 0x04 }, + { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0", endpoint_num = 0x04 }, + { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0", endpoint_num = 0x03 }, +} + +return HEIMAN_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua index 3c32e33fbe..980472671f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/heiman/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -22,25 +12,12 @@ local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration local Scenes = clusters.Scenes -local HEIMAN_GROUP_CONFIGURE = "is_group_configured" - -local HEIMAN_BUTTON_FINGERPRINTS = { - { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0", endpoint_num = 0x04 }, - { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0", endpoint_num = 0x04 }, - { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0", endpoint_num = 0x03 }, -} +local FINGERPRINTS = require("zigbee-multi-button.heiman.fingerprints") -local is_heiman_button = function(opts, driver, device) - for _, fingerprint in ipairs(HEIMAN_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end +local HEIMAN_GROUP_CONFIGURE = "is_group_configured" local function get_endpoint_num(device) - for _, fingerprint in ipairs(HEIMAN_BUTTON_FINGERPRINTS) do + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.endpoint_num end @@ -123,7 +100,7 @@ local heiman_device_handler = { } } }, - can_handle = is_heiman_button + can_handle = require("zigbee-multi-button.heiman.can_handle"), } return heiman_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua new file mode 100644 index 0000000000..1c9621e53b --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI on/off switch" then + return true, require("zigbee-multi-button.ikea.TRADFRI_on_off_switch") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua similarity index 55% rename from drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua rename to drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua index c6862ef533..87dcfcd47a 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_on_off_switch/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -33,9 +22,7 @@ local on_off_switch = { }, } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI on/off switch" - end + can_handle = require "zigbee-multi-button.ikea.TRADFRI_on_off_switch.can_handle" } return on_off_switch diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua deleted file mode 100644 index bed6086e43..0000000000 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote.lua +++ /dev/null @@ -1,36 +0,0 @@ --- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" -local button_utils = require "button_utils" - -local WindowCovering = clusters.WindowCovering - -local open_close_remote = { - NAME = "Open/Close Remote", - zigbee_handlers = { - cluster = { - [WindowCovering.ID] = { - [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), - [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI open/close remote" - end -} - -return open_close_remote diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua new file mode 100644 index 0000000000..aed0256524 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI open/close remote" then + return true, require("zigbee-multi-button.ikea.TRADFRI_open_close_remote") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua new file mode 100644 index 0000000000..23a622eb43 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_open_close_remote/init.lua @@ -0,0 +1,23 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local button_utils = require "button_utils" + +local WindowCovering = clusters.WindowCovering + +local open_close_remote = { + NAME = "Open/Close Remote", + zigbee_handlers = { + cluster = { + [WindowCovering.ID] = { + [WindowCovering.server.commands.UpOrOpen.ID] = button_utils.build_button_handler("button1", capabilities.button.button.pushed), + [WindowCovering.server.commands.DownOrClose.ID] = button_utils.build_button_handler("button2", capabilities.button.button.pushed) + } + } + }, + can_handle = require "zigbee-multi-button.ikea.TRADFRI_open_close_remote.can_handle", +} + +return open_close_remote diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua new file mode 100644 index 0000000000..d45924cfd1 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "TRADFRI remote control" then + return true, require("zigbee-multi-button.ikea.TRADFRI_remote_control") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua similarity index 81% rename from drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua rename to drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua index 0d9f12a697..bc608749b0 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/TRADFRI_remote_control/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -84,9 +73,7 @@ local remote_control = { lifecycle_handlers = { added = added_handler }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TRADFRI remote control" - end + can_handle = require "zigbee-multi-button.ikea.TRADFRI_remote_control.can_handle" } diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua new file mode 100644 index 0000000000..9e1b4676f2 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local can_handle_ikea = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.ikea.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr then + return true, require("zigbee-multi-button.ikea") + end + end + return false +end + +return can_handle_ikea diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua new file mode 100644 index 0000000000..f15d195eef --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local IKEA_MFG = { + { mfr = "IKEA of Sweden" }, + { mfr = "KE" }, + { mfr = "\02KE" } +} + +return IKEA_MFG diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua index 9a66a85991..97de983eff 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -30,20 +20,7 @@ local Groups = clusters.Groups local ENTRIES_READ = "ENTRIES_READ" -local IKEA_MFG = { - { mfr = "IKEA of Sweden" }, - { mfr = "KE" }, - { mfr = "\02KE" } -} -local can_handle_ikea = function(opts, driver, device) - for _, fingerprint in ipairs(IKEA_MFG) do - if device:get_manufacturer() == fingerprint.mfr then - return true - end - end - return false -end local do_configure = function(self, device) device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) @@ -140,12 +117,8 @@ local ikea_of_sweden = { } } }, - sub_drivers = { - require("zigbee-multi-button.ikea.TRADFRI_remote_control"), - require("zigbee-multi-button.ikea.TRADFRI_on_off_switch"), - require("zigbee-multi-button.ikea.TRADFRI_open_close_remote") - }, - can_handle = can_handle_ikea + sub_drivers = require("zigbee-multi-button.ikea.sub_drivers"), + can_handle = require("zigbee-multi-button.ikea.can_handle"), } return ikea_of_sweden diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua new file mode 100644 index 0000000000..6d7d567775 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/ikea/sub_drivers.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_sub_driver = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_remote_control"), + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_on_off_switch"), + lazy_load_sub_driver("zigbee-multi-button.ikea.TRADFRI_open_close_remote") +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 539b03785b..84dc2af26e 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -1,67 +1,13 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local supported_values = require "zigbee-multi-button.supported_values" local button_utils = require "button_utils" -local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { - { mfr = "CentraLite", model = "3450-L" }, - { mfr = "CentraLite", model = "3450-L2" }, - { mfr = "AduroSmart Eria", model = "ADUROLIGHT_CSC" }, - { mfr = "ADUROLIGHT", model = "ADUROLIGHT_CSC" }, - { mfr = "AduroSmart Eria", model = "Adurolight_NCC" }, - { mfr = "ADUROLIGHT", model = "Adurolight_NCC" }, - { mfr = "HEIMAN", model = "SceneSwitch-EM-3.0" }, - { mfr = "HEIMAN", model = "HS6SSA-W-EF-3.0" }, - { mfr = "HEIMAN", model = "HS6SSB-W-EF-3.0" }, - { mfr = "IKEA of Sweden", model = "TRADFRI on/off switch" }, - { mfr = "IKEA of Sweden", model = "TRADFRI open/close remote" }, - { mfr = "IKEA of Sweden", model = "TRADFRI remote control" }, - { mfr = "KE", model = "TRADFRI open/close remote" }, - { mfr = "\x02KE", model = "TRADFRI open/close remote" }, - { mfr = "SOMFY", model = "Situo 1 Zigbee" }, - { mfr = "SOMFY", model = "Situo 4 Zigbee" }, - { mfr = "LDS", model = "ZBT-CCTSwitch-D0001" }, - { mfr = "ShinaSystem", model = "MSM-300Z" }, - { mfr = "ShinaSystem", model = "BSM-300Z" }, - { mfr = "ShinaSystem", model = "SBM300ZB1" }, - { mfr = "ShinaSystem", model = "SBM300ZB2" }, - { mfr = "ShinaSystem", model = "SBM300ZB3" }, - { mfr = "ShinaSystem", model = "SBM300ZC1" }, - { mfr = "ShinaSystem", model = "SBM300ZC2" }, - { mfr = "ShinaSystem", model = "SBM300ZC3" }, - { mfr = "ShinaSystem", model = "SBM300ZC4" }, - { mfr = "ShinaSystem", model = "SQM300ZC4" }, - { mfr = "ROBB smarrt", model = "ROB_200-007-0" }, - { mfr = "ROBB smarrt", model = "ROB_200-008-0" }, - { mfr = "WALL HERO", model = "ACL-401SCA4" }, - { mfr = "Samsung Electronics", model = "SAMSUNG-ITM-Z-005" }, - { mfr = "Vimar", model = "RemoteControl_v1.0" }, - { mfr = "Linxura", model = "Smart Controller" }, - { mfr = "Linxura", model = "Aura Smart Button" }, - { mfr = "zunzunbee", model = "SSWZ8T" } -} -local function can_handle_zigbee_multi_button(opts, driver, device, ...) - for _, fingerprint in ipairs(ZIGBEE_MULTI_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function added_handler(self, device) local config = supported_values.get_device_parameters(device) @@ -87,22 +33,8 @@ local zigbee_multi_button = { lifecycle_handlers = { added = added_handler }, - can_handle = can_handle_zigbee_multi_button, - sub_drivers = { - require("zigbee-multi-button.ikea"), - require("zigbee-multi-button.somfy"), - require("zigbee-multi-button.ecosmart"), - require("zigbee-multi-button.centralite"), - require("zigbee-multi-button.adurosmart"), - require("zigbee-multi-button.heiman"), - require("zigbee-multi-button.shinasystems"), - require("zigbee-multi-button.robb"), - require("zigbee-multi-button.wallhero"), - require("zigbee-multi-button.SLED"), - require("zigbee-multi-button.vimar"), - require("zigbee-multi-button.linxura"), - require("zigbee-multi-button.zunzunbee") - } + can_handle = require("zigbee-multi-button.can_handle"), + sub_drivers = require("zigbee-multi-button.sub_drivers"), } return zigbee_multi_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua new file mode 100644 index 0000000000..5d8aa4bd2e --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_linxura_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.linxura.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.linxura") + end + end + return false +end + +return is_linxura_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua new file mode 100644 index 0000000000..0320a5f2dd --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LINXURA_BUTTON_FINGERPRINTS = { + { mfr = "Linxura", model = "Smart Controller"}, + { mfr = "Linxura", model = "Aura Smart Button"} +} + +return LINXURA_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua index 0e7cf44e93..cb1dd362a2 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/linxura/init.lua @@ -1,25 +1,11 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local IASZone = (require "st.zigbee.zcl.clusters").IASZone local log = require "log" -local LINXURA_BUTTON_FINGERPRINTS = { - { mfr = "Linxura", model = "Smart Controller"}, - { mfr = "Linxura", model = "Aura Smart Button"} -} local configuration = { { @@ -31,14 +17,6 @@ local configuration = { reportable_change = 1 } } -local is_linxura_button = function(opts, driver, device) - for _, fingerprint in ipairs(LINXURA_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function present_value_attr_handler(driver, device, zone_status, zb_rx) log.info("present_value_attr_handler The current value is: ", zone_status.value) @@ -84,7 +62,7 @@ local linxura_device_handler = { } }, - can_handle = is_linxura_button + can_handle = require("zigbee-multi-button.linxura.can_handle"), } return linxura_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua new file mode 100644 index 0000000000..7b30164eaa --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local ROBB_MFR_STRING = "ROBB smarrt" + local WIRELESS_REMOTE_FINGERPRINTS = require "zigbee-multi-button.robb.fingerprints" + + if device:get_manufacturer() == ROBB_MFR_STRING and WIRELESS_REMOTE_FINGERPRINTS[device:get_model()] then + return true, require("zigbee-multi-button.robb") + else + return false + end +end + +return can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua new file mode 100644 index 0000000000..0b9e1b3f51 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WIRELESS_REMOTE_FINGERPRINTS = { + ["ROB_200-008-0"] = { + endpoints = 2, + buttons = 4 + }, + ["ROB_200-007-0"] = { + endpoints = 4, + buttons = 8 + } +} + +return WIRELESS_REMOTE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua index dddef6f595..4aca7d95aa 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/robb/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -18,7 +21,6 @@ Each button-row represents one endpoint. The 8x remote control has four endpoint That means each endpoint has two buttons. --]] -local ROBB_MFR_STRING = "ROBB smarrt" local WIRELESS_REMOTE_FINGERPRINTS = { ["ROB_200-008-0"] = { endpoints = 2, @@ -30,13 +32,6 @@ local WIRELESS_REMOTE_FINGERPRINTS = { } } -local function can_handle(opts, driver, device, ...) - if device:get_manufacturer() == ROBB_MFR_STRING and WIRELESS_REMOTE_FINGERPRINTS[device:get_model()] then - return true - else - return false - end -end local button_push_handler = function(addF) return function(driver, device, zb_rx) @@ -186,7 +181,7 @@ local robb_wireless_control = { } } }, - can_handle = can_handle + can_handle = require("zigbee-multi-button.robb.can_handle"), } return robb_wireless_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua new file mode 100644 index 0000000000..d62ba886ad --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_shinasystem_button = function(opts, driver, device) + local FINGERPRINTS = require("zigbee-multi-button.shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.shinasystems") + end + end + return false +end + +return is_shinasystem_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua new file mode 100644 index 0000000000..5d0d1abb6f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/fingerprints.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SHINASYSTEM_BUTTON_FINGERPRINTS = { + { mfr = "ShinaSystem", model = "MSM-300Z", endpoint_num = 0x04 }, + { mfr = "ShinaSystem", model = "BSM-300Z", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZB1", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZB2", endpoint_num = 0x02 }, + { mfr = "ShinaSystem", model = "SBM300ZB3", endpoint_num = 0x03 }, + { mfr = "ShinaSystem", model = "SBM300ZC1", endpoint_num = 0x01 }, + { mfr = "ShinaSystem", model = "SBM300ZC2", endpoint_num = 0x02 }, + { mfr = "ShinaSystem", model = "SBM300ZC3", endpoint_num = 0x03 }, + { mfr = "ShinaSystem", model = "SBM300ZC4", endpoint_num = 0x04 }, + { mfr = "ShinaSystem", model = "SQM300ZC4", endpoint_num = 0x04 } +} + +return SHINASYSTEM_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua index 33528b3641..5d5b920db9 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/shinasystems/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,30 +10,11 @@ local OnOff = clusters.OnOff local device_management = require "st.zigbee.device_management" local Groups = clusters.Groups -local SHINASYSTEM_BUTTON_FINGERPRINTS = { - { mfr = "ShinaSystem", model = "MSM-300Z", endpoint_num = 0x04 }, - { mfr = "ShinaSystem", model = "BSM-300Z", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZB1", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZB2", endpoint_num = 0x02 }, - { mfr = "ShinaSystem", model = "SBM300ZB3", endpoint_num = 0x03 }, - { mfr = "ShinaSystem", model = "SBM300ZC1", endpoint_num = 0x01 }, - { mfr = "ShinaSystem", model = "SBM300ZC2", endpoint_num = 0x02 }, - { mfr = "ShinaSystem", model = "SBM300ZC3", endpoint_num = 0x03 }, - { mfr = "ShinaSystem", model = "SBM300ZC4", endpoint_num = 0x04 }, - { mfr = "ShinaSystem", model = "SQM300ZC4", endpoint_num = 0x04 } -} -local is_shinasystem_button = function(opts, driver, device) - for _, fingerprint in ipairs(SHINASYSTEM_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function get_ep_num_shinasystem_button(device) - for _, fingerprint in ipairs(SHINASYSTEM_BUTTON_FINGERPRINTS) do + local FINGERPRINTS = require("zigbee-multi-button.shinasystems.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then return fingerprint.endpoint_num end @@ -92,7 +63,7 @@ local shinasystem_device_handler = { } } }, - can_handle = is_shinasystem_button + can_handle = require("zigbee-multi-button.shinasystems.can_handle"), } return shinasystem_device_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua new file mode 100644 index 0000000000..55e2919fef --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function somfy_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SOMFY" then + return true, require("zigbee-multi-button.somfy") + end + return false +end + +return somfy_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua index 7236d22491..f4b7951a8f 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local constants = require "st.zigbee.constants" @@ -65,13 +55,8 @@ local somfy = { [mgmt_bind_resp.MGMT_BIND_RESPONSE] = zdo_binding_table_handler } }, - sub_drivers = { - require("zigbee-multi-button.somfy.somfy_situo_1"), - require("zigbee-multi-button.somfy.somfy_situo_4") - }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SOMFY" - end + sub_drivers = require("zigbee-multi-button.somfy.sub_drivers"), + can_handle = require("zigbee-multi-button.somfy.can_handle"), } return somfy diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua new file mode 100644 index 0000000000..45584d2f13 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "Situo 1 Zigbee" then + return true, require("zigbee-multi-button.somfy.somfy_situo_1") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua similarity index 78% rename from drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua rename to drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua index de20408306..b81791038e 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -62,9 +51,7 @@ local somfy_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "Situo 1 Zigbee" - end + can_handle = require("zigbee-multi-button.somfy.somfy_situo_1.can_handle"), } return somfy_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua new file mode 100644 index 0000000000..15b70ad23e --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/can_handle.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + if device:get_model() == "Situo 4 Zigbee" then + return true, require("zigbee-multi-button.somfy.somfy_situo_4") + end + return false +end diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua similarity index 83% rename from drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua rename to drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua index cd9c59bf90..ca7892d578 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/somfy_situo_4/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local constants = require "st.zigbee.constants" @@ -103,9 +92,7 @@ local somfy_situo_4_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "Situo 4 Zigbee" - end + can_handle = require("zigbee-multi-button.somfy.somfy_situo_4.can_handle"), } return somfy_situo_4_handler diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua new file mode 100644 index 0000000000..c1c88d1e7c --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/somfy/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_subdriver = require "lazy_load_subdriver" + +local sub_drivers = { + lazy_load_subdriver("zigbee-multi-button.somfy.somfy_situo_1"), + lazy_load_subdriver("zigbee-multi-button.somfy.somfy_situo_4"), +} + +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua new file mode 100644 index 0000000000..d8d3611ba3 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua @@ -0,0 +1,20 @@ +-- 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("zigbee-multi-button.ikea"), + lazy_load_if_possible("zigbee-multi-button.somfy"), + lazy_load_if_possible("zigbee-multi-button.ecosmart"), + lazy_load_if_possible("zigbee-multi-button.centralite"), + lazy_load_if_possible("zigbee-multi-button.adurosmart"), + lazy_load_if_possible("zigbee-multi-button.heiman"), + lazy_load_if_possible("zigbee-multi-button.shinasystems"), + lazy_load_if_possible("zigbee-multi-button.robb"), + lazy_load_if_possible("zigbee-multi-button.wallhero"), + lazy_load_if_possible("zigbee-multi-button.SLED"), + lazy_load_if_possible("zigbee-multi-button.vimar"), + lazy_load_if_possible("zigbee-multi-button.linxura"), + lazy_load_if_possible("zigbee-multi-button.zunzunbee"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua index 172a7c1ca3..813859f891 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/supported_values.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local devices = { BUTTON_PUSH_HELD_2 = { diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua new file mode 100644 index 0000000000..8317f95893 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function vimar_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Vimar" and device:get_model() == "RemoteControl_v1.0" then + return true, require("zigbee-multi-button.vimar") + end + return false +end + +return vimar_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua index 3df8cc4d71..aa7ba729a1 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -85,9 +75,7 @@ local vimar_remote_control = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Vimar" and device:get_model() == "RemoteControl_v1.0" - end + can_handle = require("zigbee-multi-button.vimar.can_handle"), } return vimar_remote_control diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua new file mode 100644 index 0000000000..633871874d --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_wallhero_button(opts, driver, device, ...) + local FINGERPRINTS = require("zigbee-multi-button.wallhero.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.wallhero") + end + end + return false +end + +return can_handle_wallhero_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua new file mode 100644 index 0000000000..6b6ca8b00f --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "WALL HERO", model = "ACL-401SCA4" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua index 71b01c84f8..e562aef77d 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/wallhero/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local log = require "log" @@ -19,18 +9,7 @@ local zcl_clusters = require "st.zigbee.zcl.clusters" local Scenes = zcl_clusters.Scenes -local FINGERPRINTS = { - { mfr = "WALL HERO", model = "ACL-401SCA4" } -} -local function can_handle_wallhero_button(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 scenes_cluster_handler(driver, device, zb_rx) local additional_fields = { @@ -97,7 +76,7 @@ local wallhero_button = { } } }, - can_handle = can_handle_wallhero_button + can_handle = require("zigbee-multi-button.wallhero.can_handle"), } return wallhero_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua new file mode 100644 index 0000000000..0277725380 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Check if a given device matches the supported fingerprints +local function is_zunzunbee_button(opts, driver, device) + local ZUNZUNBEE_BUTTON_FINGERPRINTS = require "zigbee-multi-button.zunzunbee.fingerprints" + for _, fingerprint in ipairs(ZUNZUNBEE_BUTTON_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.zunzunbee") + end + end + return false +end + +return is_zunzunbee_button diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua new file mode 100644 index 0000000000..b893f4c1e6 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- List of supported device fingerprints +local ZUNZUNBEE_BUTTON_FINGERPRINTS = { + { mfr = "zunzunbee", model = "SSWZ8T" } +} + +return ZUNZUNBEE_BUTTON_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua index 7bb209a622..e8fa24a63e 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/zunzunbee/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local Tone = capabilities.tone @@ -30,26 +20,11 @@ local battery_config = utils.deep_copy(battery_defaults.default_percentage_confi battery_config.reportable_change = 0x02 battery_config.data_type = clusters.PowerConfiguration.attributes.BatteryVoltage.base_type --- List of supported device fingerprints -local ZUNZUNBEE_BUTTON_FINGERPRINTS = { - { mfr = "zunzunbee", model = "SSWZ8T" } -} - -- Initialize device attributes local function init_handler(self, device) device:add_configured_attribute(battery_config) end --- Check if a given device matches the supported fingerprints -local function is_zunzunbee_button(opts, driver, device) - for _, fingerprint in ipairs(ZUNZUNBEE_BUTTON_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -- Generate and emit button events based on IAS Zone status attribute local function generate_button_event_from_zone_status(driver, device, zone_status, zb_rx) local raw_value = tonumber(zone_status.value) @@ -163,7 +138,7 @@ local zunzunbee_device_handler = { } } }, - can_handle = is_zunzunbee_button + can_handle = require("zigbee-multi-button.zunzunbee.can_handle"), } return zunzunbee_device_handler From 84747bd4b6162caca6c28f58d6c90f02909b0990 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:10:23 -0600 Subject: [PATCH 419/449] Matter Switch: Include Ikea subdriver support for knob capability (#2678) --- .../matter-switch/profiles/ikea-scroll.yml | 6 + .../src/sub_drivers/ikea_scroll/init.lua | 10 + .../scroll_handlers/event_handlers.lua | 58 +++ .../scroll_utils/device_configuration.lua | 11 +- .../ikea_scroll/scroll_utils/fields.lua | 29 +- .../ikea_scroll/scroll_utils/utils.lua | 18 +- .../matter-switch/src/switch_utils/utils.lua | 20 +- .../src/test/test_ikea_scroll.lua | 415 +++++++++++++++++- 8 files changed, 545 insertions(+), 22 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index 4dc77d026d..cce4b68d2d 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -5,6 +5,8 @@ components: capabilities: - id: button version: 1 + - id: knob + version: 1 - id: battery version: 1 - id: firmwareUpdate @@ -18,6 +20,8 @@ components: capabilities: - id: button version: 1 + - id: knob + version: 1 categories: - name: RemoteController - id: group3 @@ -25,5 +29,7 @@ components: capabilities: - id: button version: 1 + - id: knob + version: 1 categories: - name: RemoteController diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua index 6e21a56ac0..5c2a009de1 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -1,9 +1,11 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local clusters = require "st.matter.clusters" local switch_utils = require "switch_utils.utils" local scroll_utils = require "sub_drivers.ikea_scroll.scroll_utils.utils" local scroll_cfg = require "sub_drivers.ikea_scroll.scroll_utils.device_configuration" +local event_handlers = require "sub_drivers.ikea_scroll.scroll_handlers.event_handlers" local IkeaScrollLifecycleHandlers = {} @@ -44,6 +46,14 @@ local ikea_scroll_handler = { infoChanged = IkeaScrollLifecycleHandlers.info_changed, init = IkeaScrollLifecycleHandlers.device_init, }, + matter_handlers = { + event = { + [clusters.Switch.ID] = { + [clusters.Switch.events.MultiPressOngoing.ID] = event_handlers.multi_press_ongoing_handler, + [clusters.Switch.events.MultiPressComplete.ID] = event_handlers.multi_press_complete_handler, + } + } + }, can_handle = require("sub_drivers.ikea_scroll.can_handle") } 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 new file mode 100644 index 0000000000..7415da8f9b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua @@ -0,0 +1,58 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local st_utils = require "st.utils" +local capabilities = require "st.capabilities" +local switch_utils = require "switch_utils.utils" +local generic_event_handlers = require "switch_handlers.event_handlers" +local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" + +local IkeaScrollEventHandlers = {} + +local function rotate_amount_event_helper(device, endpoint_id, num_presses_to_handle) + -- 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) + device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true})) +end + +-- Used by ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL, not ENDPOINTS_PUSH +function IkeaScrollEventHandlers.multi_press_ongoing_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + -- Ignore MultiPressOngoing events from push endpoints. + device.log.debug("Received MultiPressOngoing event from push endpoint, ignoring.") + else + local cur_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.current_number_of_presses_counted.value or 0 + local num_presses_to_handle = cur_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) + if num_presses_to_handle > 0 then + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, cur_num_presses_counted) + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + end + end +end + +function IkeaScrollEventHandlers.multi_press_complete_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + generic_event_handlers.multi_press_complete_handler(driver, device, ib, response) + else + local total_num_presses_counted = ib.data and ib.data.elements and ib.data.elements.total_number_of_presses_counted.value or 0 + local num_presses_to_handle = total_num_presses_counted - (device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0) + if num_presses_to_handle > 0 then + rotate_amount_event_helper(device, ib.endpoint_id, num_presses_to_handle) + end + -- reset the LATEST_NUMBER_OF_PRESSES_COUNTED to nil at the end of a MultiPress chain. + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, nil) + end +end + +function IkeaScrollEventHandlers.initial_press_handler(driver, device, ib, response) + if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then + generic_event_handlers.initial_press_handler(driver, device, ib, response) + else + -- Ignore InitialPress events from non-push endpoints. Presently, we want to solely + -- rely on MultiPressOngoing events to handle rotation for those endpoints. + device.log.debug("Received InitialPress event from scroll endpoint, ignoring.") + end +end + +return IkeaScrollEventHandlers diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua index cd2cee49ce..457804d495 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua @@ -11,19 +11,22 @@ local IkeaScrollConfiguration = {} function IkeaScrollConfiguration.build_button_component_map(device) local component_map = { - main = scroll_fields.ENDPOINTS_PRESS[1], - group2 = scroll_fields.ENDPOINTS_PRESS[2], - group3 = scroll_fields.ENDPOINTS_PRESS[3], + main = {scroll_fields.ENDPOINTS_PUSH[1], scroll_fields.ENDPOINTS_UP_SCROLL[1], scroll_fields.ENDPOINTS_DOWN_SCROLL[1]}, + group2 = {scroll_fields.ENDPOINTS_PUSH[2], scroll_fields.ENDPOINTS_UP_SCROLL[2], scroll_fields.ENDPOINTS_DOWN_SCROLL[2]}, + group3 = {scroll_fields.ENDPOINTS_PUSH[3], scroll_fields.ENDPOINTS_UP_SCROLL[3], scroll_fields.ENDPOINTS_DOWN_SCROLL[3]}, } device:set_field(switch_fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) end function IkeaScrollConfiguration.configure_buttons(device) - for _, ep in ipairs(scroll_fields.ENDPOINTS_PRESS) do + for _, ep in ipairs(scroll_fields.ENDPOINTS_PUSH) do device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) end + for _, ep in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do -- and by extension, ENDPOINTS_DOWN_SCROLL + device:emit_event_for_endpoint(ep, capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}})) + end end function IkeaScrollConfiguration.match_profile(driver, device) 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 fff0a1cce4..58580328b1 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 @@ -1,6 +1,7 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local IkeaScrollFields = {} @@ -8,14 +9,36 @@ local IkeaScrollFields = {} -- PowerSource supported on Root Node IkeaScrollFields.ENDPOINT_POWER_SOURCE = 0 --- Switch Endpoints used for basic press functionality -IkeaScrollFields.ENDPOINTS_PRESS = {3, 6, 9} +-- Generic Switch Endpoints used for basic push functionality +IkeaScrollFields.ENDPOINTS_PUSH = {3, 6, 9} --- Required Events for the ENDPOINTS_PRESS. +-- Generic Switch Endpoints used for Up Scroll functionality +IkeaScrollFields.ENDPOINTS_UP_SCROLL = {1, 4, 7} + +-- Generic Switch Endpoints used for Down Scroll functionality +IkeaScrollFields.ENDPOINTS_DOWN_SCROLL = {2, 5, 8} + +-- Maximum number of presses at a time +IkeaScrollFields.MAX_SCROLL_PRESSES = 18 + +-- Amount to rotate per scroll event +IkeaScrollFields.PER_SCROLL_EVENT_ROTATION = st_utils.round(1 / IkeaScrollFields.MAX_SCROLL_PRESSES * 100) + +-- Field to track the latest number of presses counted during a single scroll event sequence +IkeaScrollFields.LATEST_NUMBER_OF_PRESSES_COUNTED = "__latest_number_of_presses_counted" + +-- Required Events for the ENDPOINTS_PUSH. IkeaScrollFields.switch_press_subscribed_events = { clusters.Switch.events.InitialPress.ID, clusters.Switch.events.MultiPressComplete.ID, clusters.Switch.events.LongPress.ID, } +-- Required Events for the ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL. Adds a +-- MultiPressOngoing subscription to handle step functionality in real-time +IkeaScrollFields.switch_scroll_subscribed_events = { + clusters.Switch.events.MultiPressOngoing.ID, + clusters.Switch.events.MultiPressComplete.ID, +} + return IkeaScrollFields diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua index 67ba2acba5..9a9f95228b 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/utils.lua @@ -7,12 +7,24 @@ local scroll_fields = require "sub_drivers.ikea_scroll.scroll_utils.fields" local IkeaScrollUtils = {} --- override subscribe function to prevent subscribing to additional events from the main driver +-- override subscribe function in the main driver function IkeaScrollUtils.subscribe(device) local subscribe_request = im.InteractionRequest(im.InteractionRequest.RequestType.SUBSCRIBE, {}) - for _, ep_press in ipairs(scroll_fields.ENDPOINTS_PRESS) do + for _, ep_push in ipairs(scroll_fields.ENDPOINTS_PUSH) do for _, switch_event in ipairs(scroll_fields.switch_press_subscribed_events) do - local ib = im.InteractionInfoBlock(ep_press, clusters.Switch.ID, nil, switch_event) + local ib = im.InteractionInfoBlock(ep_push, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + for _, ep_up in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do + for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_up, clusters.Switch.ID, nil, switch_event) + subscribe_request:with_info_block(ib) + end + end + for _, ep_down in ipairs(scroll_fields.ENDPOINTS_DOWN_SCROLL) do + for _, switch_event in ipairs(scroll_fields.switch_scroll_subscribed_events) do + local ib = im.InteractionInfoBlock(ep_down, clusters.Switch.ID, nil, switch_event) subscribe_request:with_info_block(ib) end end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index da5376f031..0592d9a342 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -207,7 +207,8 @@ end --- An extension of the library function endpoint_to_component, used to support a mapping scheme --- that optionally includes cluster and attribute ids so that multiple components can be mapped ---- to a single endpoint. +--- to a single endpoint. This extension also handles the case that multiple endpoints map to the +--- same component --- --- @param device any a Matter device object --- @param ep_info number|table either an ep_id or a table { endpoint_id, optional(cluster_id), optional(attribute_id) } @@ -220,10 +221,19 @@ function utils.endpoint_to_component(device, ep_info) for component, map_info in pairs(device:get_field(fields.COMPONENT_TO_ENDPOINT_MAP) or {}) do if type(map_info) == "number" and map_info == ep_info.endpoint_id then return component - elseif type(map_info) == "table" and map_info.endpoint_id == ep_info.endpoint_id - and (not map_info.cluster_id or (map_info.cluster_id == ep_info.cluster_id - and (not map_info.attribute_ids or utils.tbl_contains(map_info.attribute_ids, ep_info.attribute_id)))) then - return component + elseif type(map_info) == "table" then + if type(map_info.endpoint_id) == "number" then + map_info = {map_info} + end + for _, ep_map_info in ipairs(map_info) do + if type(ep_map_info) == "number" and ep_map_info == ep_info.endpoint_id then + return component + elseif type(ep_map_info) == "table" and ep_map_info.endpoint_id == ep_info.endpoint_id + and (not ep_map_info.cluster_id or (ep_map_info.cluster_id == ep_info.cluster_id + and (not ep_map_info.attribute_ids or utils.tbl_contains(ep_map_info.attribute_ids, ep_info.attribute_id)))) then + return component + end + end end end return "main" 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 c560386e37..78f99c37fe 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -122,18 +122,28 @@ local mock_ikea_scroll = test.mock_device.build_test_matter_device({ } }) -local ENDPOINTS_PRESS = { 3, 6, 9 } +local ENDPOINTS_PUSH = { 3, 6, 9 } +local ENDPOINTS_SCROLL = {1, 2, 4, 5, 7, 8} -- the ikea scroll subdriver has overriden subscribe behavior local function ikea_scroll_subscribe() - local CLUSTER_SUBSCRIBE_LIST ={ - clusters.Switch.events.InitialPress, + local CLUSTER_SUBSCRIBE_LIST_PUSH ={ + clusters.Switch.events.InitialPress, clusters.Switch.server.events.LongPress, clusters.Switch.server.events.MultiPressComplete, } - local subscribe_request = CLUSTER_SUBSCRIBE_LIST[1]:subscribe(mock_ikea_scroll, ENDPOINTS_PRESS[1]) - for _, ep_press in ipairs(ENDPOINTS_PRESS) do - for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST) do + local CLUSTER_SUBSCRIBE_LIST_SCROLL = { + clusters.Switch.server.events.MultiPressOngoing, + clusters.Switch.server.events.MultiPressComplete, + } + local subscribe_request = CLUSTER_SUBSCRIBE_LIST_PUSH[1]:subscribe(mock_ikea_scroll, ENDPOINTS_PUSH[1]) + for _, ep_press in ipairs(ENDPOINTS_PUSH) do + for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST_PUSH) do + subscribe_request:merge(event:subscribe(mock_ikea_scroll, ep_press)) + end + end + for _, ep_press in ipairs(ENDPOINTS_SCROLL) do + for _, event in ipairs(CLUSTER_SUBSCRIBE_LIST_SCROLL) do subscribe_request:merge(event:subscribe(mock_ikea_scroll, ep_press)) end end @@ -149,6 +159,9 @@ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", button_attr.pushed({state_change = false}))) test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 9)}) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", button_attr.pushed({state_change = false}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) end local function test_init() @@ -221,4 +234,392 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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.register_message_test( + "Ikea Scroll Negative rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-18, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }, + } + } +) + +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Negative rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Positive rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + 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} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Negative rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {current_number_of_presses_counted = 2, new_position = 2} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-12, {state_change = true})) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {current_number_of_presses_counted = 5, new_position = 5} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-18, {state_change = true})) + } + } +) + +test.register_message_test( + "Ikea Scroll Long Press Push events on main are handled correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[1], {new_position = 1} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.LongPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.button.button.held({state_change = true})) + }, + } +) + +test.register_message_test( + "Ikea Scroll MultiPressComplete Push events on group2 are handled correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[2], {new_position = 1} + ) + }, + }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_PUSH[2], {total_number_of_presses_counted = 1, previous_position = 0} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.button.button.pushed({state_change = true})) + }, + } +) + +test.run_registered_tests() From 48b01998512145c9755d03d0381136aa4fada002 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Wed, 11 Feb 2026 15:30:19 -0800 Subject: [PATCH 420/449] WWSTCERT-10319 GELUBU Door Lock S93 --- drivers/SmartThings/zigbee-lock/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-lock/fingerprints.yml b/drivers/SmartThings/zigbee-lock/fingerprints.yml index 86870aa962..5c04d78ce5 100644 --- a/drivers/SmartThings/zigbee-lock/fingerprints.yml +++ b/drivers/SmartThings/zigbee-lock/fingerprints.yml @@ -1,4 +1,10 @@ zigbeeManufacturer: + # GELUBU + - id: "GELUBU/S93" + deviceLabel: GELUBU Door Lock S93 + manufacturer: GELUBU + model: S93 + deviceProfileName: lock-battery # YALE - id: "Yale YRD220/240" deviceLabel: "Yale Door Lock" From 2779abc23d10beb89fcb916fb817fcfd9143070b Mon Sep 17 00:00:00 2001 From: Hunsup Jung Date: Tue, 3 Feb 2026 13:59:41 +0900 Subject: [PATCH 421/449] Add vid/pid of the Schlage Sense Pro to new-matter-lock Signed-off-by: Hunsup Jung --- .../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 bddc7b1fd1..799c20b9ae 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -30,6 +30,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x10E1, 0x2002}, -- VDA {0x1421, 0x0042}, -- Kwikset Halo Select Plus {0x1421, 0x0081}, -- Kwikset Aura Reach + {0x1236, 0xa538}, -- Schlage Sense Pro } return NEW_MATTER_LOCK_PRODUCTS From 19588410e5b83f8c9f2703643c83f6891e9dc227 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:11 -0600 Subject: [PATCH 422/449] CHAD-17077: zigbee-thermostat lazy load subdrivers --- .../src/aqara/can_handle.lua | 14 ++++++++ .../src/aqara/fingerprints.lua | 8 +++++ .../zigbee-thermostat/src/aqara/init.lua | 31 +++------------- .../src/danfoss/can_handle.lua | 14 ++++++++ .../src/danfoss/fingerprints.lua | 8 +++++ .../zigbee-thermostat/src/danfoss/init.lua | 18 +++------- .../src/fidure/can_handle.lua | 11 ++++++ .../zigbee-thermostat/src/fidure/init.lua | 20 +++-------- .../zigbee-thermostat/src/init.lua | 31 +++------------- .../src/lazy_load_subdriver.lua | 15 ++++++++ .../src/leviton/can_handle.lua | 11 ++++++ .../zigbee-thermostat/src/leviton/init.lua | 20 +++-------- .../src/lux-konoz/can_handle.lua | 14 ++++++++ .../src/lux-konoz/fingerprints.lua | 8 +++++ .../zigbee-thermostat/src/lux-konoz/init.lua | 29 +++------------ .../zigbee-thermostat/src/popp/can_handle.lua | 14 ++++++++ .../src/popp/fingerprints.lua | 15 ++++++++ .../zigbee-thermostat/src/popp/init.lua | 36 +++---------------- .../src/resideo_korea/can_handle.lua | 11 ++++++ .../src/resideo_korea/init.lua | 20 +++-------- .../src/sinope/can_handle.lua | 13 +++++++ .../zigbee-thermostat/src/sinope/init.lua | 27 +++----------- .../can_handle.lua | 14 ++++++++ .../fingerprints.lua | 9 +++++ .../src/stelpro-ki-zigbee-thermostat/init.lua | 30 +++------------- .../src/stelpro/can_handle.lua | 14 ++++++++ .../src/stelpro/fingerprints.lua | 10 ++++++ .../zigbee-thermostat/src/stelpro/init.lua | 33 +++-------------- .../stelpro_maestrostat/can_handle.lua | 14 ++++++++ .../stelpro_maestrostat/fingerprints.lua | 6 ++++ .../init.lua} | 30 ++-------------- .../src/stelpro/stelpro_sorb/can_handle.lua | 14 ++++++++ .../src/stelpro/stelpro_sorb/fingerprints.lua | 7 ++++ .../init.lua} | 31 ++-------------- .../src/stelpro/sub_drivers.lua | 9 +++++ .../zigbee-thermostat/src/sub_drivers.lua | 19 ++++++++++ .../src/test/test_aqara_thermostat.lua | 15 ++------ .../src/test/test_centralite_thermostat.lua | 15 ++------ .../src/test/test_danfoss_thermostat.lua | 5 ++- .../src/test/test_fidure_thermostat.lua | 15 ++------ .../src/test/test_leviton_rc.lua | 15 ++------ .../src/test/test_popp_thermostat.lua | 3 ++ .../src/test/test_resideo_dt300st_m000.lua | 15 ++------ .../test/test_sinope_th1300_thermostat.lua | 15 ++------ .../test/test_sinope_th1400_thermostat.lua | 15 ++------ .../src/test/test_sinope_thermostat.lua | 15 ++------ .../test_stelpro_ki_zigbee_thermostat.lua | 15 ++------ .../src/test/test_stelpro_thermostat.lua | 15 ++------ .../src/test/test_vimar_thermostat.lua | 15 ++------ .../src/test/test_zenwithin_thermostat.lua | 15 ++------ .../src/test/test_zigbee_thermostat.lua | 15 ++------ .../src/vimar/can_handle.lua | 17 +++++++++ .../zigbee-thermostat/src/vimar/init.lua | 27 +++----------- .../src/zenwithin/can_handle.lua | 11 ++++++ .../zigbee-thermostat/src/zenwithin/init.lua | 20 +++-------- 55 files changed, 394 insertions(+), 512 deletions(-) create mode 100644 drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua rename drivers/SmartThings/zigbee-thermostat/src/stelpro/{stelpro_maestrostat.lua => stelpro_maestrostat/init.lua} (67%) create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua rename drivers/SmartThings/zigbee-thermostat/src/stelpro/{stelpro_sorb.lua => stelpro_sorb/init.lua} (66%) create mode 100644 drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..30f1243e76 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.airrtc.agl001" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua index 662d337b82..b17b3d9e1c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/aqara/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -34,9 +24,6 @@ local PRIVATE_ANTIFREEZE_MODE_TEMPERATURE_SETTING_ID = 0x0279 local PRIVATE_VALVE_RESULT_CALIBRATION_ID = 0x027B local PRIVATE_BATTERY_ENERGY_ID = 0x040A -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.airrtc.agl001" } -} local preference_map = { ["stse.notificationOfValveTest"] = { @@ -82,14 +69,6 @@ local function device_info_changed(driver, device, event, args) end end -local function is_aqara_products(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 supported_thermostat_modes_handler(driver, device, value) device:emit_event(capabilities.thermostatMode.supportedThermostatModes({ @@ -277,7 +256,7 @@ local aqara_radiator_thermostat_e1_handler = { [capabilities.refresh.commands.refresh.NAME] = do_refresh, } }, - can_handle = is_aqara_products + can_handle = require("aqara.can_handle"), } -return aqara_radiator_thermostat_e1_handler \ No newline at end of file +return aqara_radiator_thermostat_e1_handler diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua new file mode 100644 index 0000000000..2f9ba97b72 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_danfoss_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("danfoss.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("danfoss") + end + end + return false +end + +return is_danfoss_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua new file mode 100644 index 0000000000..26386b21a9 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local DANFOSS_THERMOSTAT_FINGERPRINTS = { + { mfr = "Danfoss", model = "eTRV0100" } +} + +return DANFOSS_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua b/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua index c5b181955f..f5b0d5434c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/danfoss/init.lua @@ -1,19 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local PowerConfiguration = clusters.PowerConfiguration -local DANFOSS_THERMOSTAT_FINGERPRINTS = { - { mfr = "Danfoss", model = "eTRV0100" } -} -local is_danfoss_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(DANFOSS_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local danfoss_thermostat = { NAME = "Danfoss Thermostat Handler", @@ -27,7 +19,7 @@ local danfoss_thermostat = { lifecycle_handlers = { init = battery_defaults.build_linear_voltage_init(2.4, 3.2) }, - can_handle = is_danfoss_thermostat + can_handle = require("danfoss.can_handle"), } -return danfoss_thermostat \ No newline at end of file +return danfoss_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua new file mode 100644 index 0000000000..24ee0ea0e2 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/fidure/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function fidure_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Fidure" and device:get_model() == "A1732R3" then + return true, require("fidure") + end + return false +end + +return fidure_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua b/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua index 8b46c65e73..5d87089aa1 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/fidure/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" @@ -38,9 +28,7 @@ local fidure_thermostat = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Fidure" and device:get_model() == "A1732R3" - end + can_handle = require("fidure.can_handle"), } return fidure_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index 0a5e82350e..b1766e7892 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local ZigbeeDriver = require "st.zigbee" @@ -362,20 +352,7 @@ local zigbee_thermostat_driver = { doConfigure = do_configure, added = device_added }, - sub_drivers = { - require("zenwithin"), - require("fidure"), - require("sinope"), - require("stelpro-ki-zigbee-thermostat"), - require("stelpro"), - require("lux-konoz"), - require("leviton"), - require("danfoss"), - require("popp"), - require("vimar"), - require("resideo_korea"), - require("aqara") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua new file mode 100644 index 0000000000..054aaa821b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/leviton/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function leviton_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "HAI" and device:get_model() == "65A01-1" then + return true, require("leviton") + end + return false +end + +return leviton_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua b/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua index 9b1150ae25..4b35916229 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/leviton/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local Thermostat = clusters.Thermostat @@ -128,9 +118,7 @@ local leviton_thermostat = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "HAI" and device:get_model() == "65A01-1" - end + can_handle = require("leviton.can_handle"), } return leviton_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua new file mode 100644 index 0000000000..b7f7e5220f --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_lux_konoz = function(opts, driver, device) + local FINGERPRINTS = require("lux-konoz.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("lux-konoz") + end + end + return false +end + +return is_lux_konoz diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua new file mode 100644 index 0000000000..01bb0bac8c --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LUX_KONOZ_THERMOSTAT_FINGERPRINTS = { + { mfr = "LUX", model = "KONOZ" } +} + +return LUX_KONOZ_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua index 815639985c..0609128ac2 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/lux-konoz/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local Thermostat = clusters.Thermostat @@ -19,18 +9,7 @@ local capabilities = require "st.capabilities" local ThermostatMode = capabilities.thermostatMode -local LUX_KONOZ_THERMOSTAT_FINGERPRINTS = { - { mfr = "LUX", model = "KONOZ" } -} -local is_lux_konoz = function(opts, driver, device) - for _, fingerprint in ipairs(LUX_KONOZ_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- LUX KONOz reports extra ["auto", "emergency heat"] which, actually, aren't supported local supported_thermostat_modes_handler = function(driver, device, supported_modes) @@ -46,7 +25,7 @@ local lux_konoz = { } } }, - can_handle = is_lux_konoz + can_handle = require("lux-konoz.can_handle"), } return lux_konoz diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua new file mode 100644 index 0000000000..907350cd1c --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_popp_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("popp.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("popp") + end + end + return false +end + +return is_popp_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua new file mode 100644 index 0000000000..16a5cc0942 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local POPP_THERMOSTAT_FINGERPRINTS = { + { + mfr = "D5X84YU", + model = "eT093WRO" + }, + { + mfr = "D5X84YU", + model = "eT093WRG" + } +} + +return POPP_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua index 007fb722b0..f842fe8c2b 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/popp/init.lua @@ -1,17 +1,6 @@ --- Copyright 2023 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. --- Zigbee driver utilities +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local battery_defaults = require "st.zigbee.defaults.battery_defaults" local data_types = require "st.zigbee.data_types" @@ -34,14 +23,6 @@ local ThermostatMode = capabilities.thermostatMode local TemperatureAlarm = capabilities.temperatureAlarm local Switch = capabilities.switch -local POPP_THERMOSTAT_FINGERPRINTS = { { - mfr = "D5X84YU", - model = "eT093WRO" -}, { - mfr = "D5X84YU", - model = "eT093WRG" -} } - local STORED_HEAT_MODE = "stored_heat_mode" local MFG_CODE = 0x1246 @@ -114,15 +95,6 @@ local PREFERENCE_TABLES = { local SUPPORTED_MODES = { ThermostatMode.thermostatMode.heat.NAME, ThermostatMode.thermostatMode.eco.NAME } -local is_popp_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(POPP_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - -- Helpers -- has member check function @@ -425,7 +397,7 @@ local popp_thermostat = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = is_popp_thermostat + can_handle = require("popp.can_handle"), } return popp_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua new file mode 100644 index 0000000000..94eee72e87 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function resideo_korea_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Resideo Korea" and device:get_model() == "DT300ST-M000" then + return true, require("resideo_korea") + end + return false +end + +return resideo_korea_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua index a1e68ef02a..14221dd223 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/resideo_korea/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_lib = require "st.device" local device_management = require "st.zigbee.device_management" @@ -178,9 +168,7 @@ local resideo_thermostat = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Resideo Korea" and device:get_model() == "DT300ST-M000" - end + can_handle = require("resideo_korea.can_handle"), } return resideo_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua new file mode 100644 index 0000000000..fdc43dab3b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/sinope/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_sinope_thermostat = function(opts, driver, device) + local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" + if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then + return true, require("sinope") + else + return false + end +end + +return is_sinope_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua b/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua index bef41f2b9c..7926b1e0c7 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/sinope/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -26,8 +16,6 @@ local ThermostatOperatingState = capabilities.thermostatOperatingState local ThermostatHeatingSetpoint = capabilities.thermostatHeatingSetpoint local TemperatureMeasurement = capabilities.temperatureMeasurement -local SINOPE_TECHNOLOGIES_MFR_STRING = "Sinope Technologies" - local SINOPE_CUSTOM_CLUSTER = 0xFF01 local MFR_TIME_FORMAT_ATTRIBUTE = 0x0114 local MFR_AIR_FLOOR_MODE_ATTRIBUTE = 0x0105 @@ -91,13 +79,6 @@ local PREFERENCE_TABLES = { } } -local is_sinope_thermostat = function(opts, driver, device) - if device:get_manufacturer() == SINOPE_TECHNOLOGIES_MFR_STRING then - return true - else - return false - end -end local do_refresh = function(self, device) local attributes = { @@ -176,7 +157,7 @@ local sinope_thermostat = { doConfigure = do_configure, infoChanged = info_changed }, - can_handle = is_sinope_thermostat + can_handle = require("sinope.can_handle"), } return sinope_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua new file mode 100644 index 0000000000..3f0bebb126 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_ki_zigbee_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro-ki-zigbee-thermostat.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro-ki-zigbee-thermostat") + end + end + return false +end + +return is_stelpro_ki_zigbee_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua new file mode 100644 index 0000000000..2cf1a884d7 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS = { + { mfr = "Stelpro", model = "STZB402+" }, + { mfr = "Stelpro", model = "ST218" }, +} + +return STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua index bcc09b271c..7dd1d5f0d2 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro-ki-zigbee-thermostat/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" @@ -30,10 +20,6 @@ local ThermostatHeatingSetpoint = capabilities.thermostatHeatingSetpoint local TemperatureMeasurement = capabilities.temperatureMeasurement local TemperatureAlarm = capabilities.temperatureAlarm -local STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "STZB402+" }, - { mfr = "Stelpro", model = "ST218" }, -} -- The Groovy DTH stored the raw Celsius values because it was responsible for converting -- to Farenheit if the user's location necessitated. Right now the driver only operates @@ -60,14 +46,6 @@ local THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.eco } -local is_stelpro_ki_zigbee_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_KI_ZIGBEE_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function has_member(haystack, needle) for _, value in ipairs(haystack) do @@ -342,7 +320,7 @@ local stelpro_ki_zigbee_thermostat = { added = device_added, doConfigure = do_configure }, - can_handle = is_stelpro_ki_zigbee_thermostat + can_handle = require("stelpro-ki-zigbee-thermostat.can_handle"), } return stelpro_ki_zigbee_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua new file mode 100644 index 0000000000..117d8ca51d --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro") + end + end + return false +end + +return is_stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua new file mode 100644 index 0000000000..9271bba198 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local STELPRO_THERMOSTAT_FINGERPRINTS = { + { mfr = "Stelpro", model = "MaestroStat" }, + { mfr = "Stelpro", model = "SORB" }, + { mfr = "Stelpro", model = "SonomaStyle" } +} + +return STELPRO_THERMOSTAT_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua index 063f423517..d186c46b67 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -25,20 +15,7 @@ local RX_HEAT_VALUE = 0x7fff local FREEZE_ALRAM_TEMPERATURE = 0 local HEAT_ALRAM_TEMPERATURE = 50 -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "MaestroStat" }, - { mfr = "Stelpro", model = "SORB" }, - { mfr = "Stelpro", model = "SonomaStyle" } -} -local is_stelpro_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function get_temperature(temperature) return temperature / 100 @@ -128,8 +105,8 @@ local stelpro_thermostat = { added = device_added, infoChanged = info_changed }, - sub_drivers = { require("stelpro.stelpro_sorb"), require("stelpro.stelpro_maestrostat") }, - can_handle = is_stelpro_thermostat + sub_drivers = require("stelpro.sub_drivers"), + can_handle = require("stelpro.can_handle"), } return stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua new file mode 100644 index 0000000000..f9410f80ff --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_thermostat = function(opts, driver, device) + local FINGERPRINTS = require "stelpro.stelpro_maestrostat.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro.stelpro_maestrostat") + end + end + return false +end + +return is_stelpro_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua new file mode 100644 index 0000000000..600fe09180 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Stelpro", model = "MaestroStat" }, +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua similarity index 67% rename from drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua rename to drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua index c4d81b944a..fecb34666a 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_maestrostat/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,19 +9,6 @@ local RelativeHumidity = clusters.RelativeHumidity local Thermostat = clusters.Thermostat local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "MaestroStat" }, -} - -local is_stelpro_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local do_refresh = function(self, device) local attributes = { Thermostat.attributes.LocalTemperature, @@ -74,7 +50,7 @@ local stelpro_maestro_othermostat = { added = device_added, doConfigure = do_configure }, - can_handle = is_stelpro_thermostat + can_handle = require("stelpro.stelpro_maestrostat.can_handle") } return stelpro_maestro_othermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua new file mode 100644 index 0000000000..d9e99178e5 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_stelpro_sorb_thermostat = function(opts, driver, device) + local FINGERPRINTS = require("stelpro.stelpro_sorb.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("stelpro.stelpro_sorb") + end + end + return false +end + +return is_stelpro_sorb_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua new file mode 100644 index 0000000000..eb8731f6dc --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/fingerprints.lua @@ -0,0 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "Stelpro", model = "SORB" }, + { mfr = "Stelpro", model = "SonomaStyle" } +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua similarity index 66% rename from drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua rename to drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua index 504d4351e0..39c370960f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/stelpro_sorb/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" @@ -20,20 +9,6 @@ local RelativeHumidity = clusters.RelativeHumidity local Thermostat = clusters.Thermostat local ThermostatUserInterfaceConfiguration = clusters.ThermostatUserInterfaceConfiguration -local STELPRO_THERMOSTAT_FINGERPRINTS = { - { mfr = "Stelpro", model = "SORB" }, - { mfr = "Stelpro", model = "SonomaStyle" } -} - -local is_stelpro_sorb_thermostat = function(opts, driver, device) - for _, fingerprint in ipairs(STELPRO_THERMOSTAT_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end - local do_refresh = function(self, device) local attributes = { Thermostat.attributes.LocalTemperature, @@ -75,7 +50,7 @@ local stelpro_sorb_thermostat = { added = device_added, doConfigure = do_configure }, - can_handle = is_stelpro_sorb_thermostat + can_handle = require("stelpro.stelpro_sorb.can_handle") } return stelpro_sorb_thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua new file mode 100644 index 0000000000..aa48c7eda6 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/stelpro/sub_drivers.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("stelpro.stelpro_sorb"), + lazy_load_if_possible("stelpro.stelpro_maestrostat") +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua new file mode 100644 index 0000000000..7f62589c24 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua @@ -0,0 +1,19 @@ +-- 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("zenwithin"), + lazy_load_if_possible("fidure"), + lazy_load_if_possible("sinope"), + lazy_load_if_possible("stelpro-ki-zigbee-thermostat"), + lazy_load_if_possible("stelpro"), + lazy_load_if_possible("lux-konoz"), + lazy_load_if_possible("leviton"), + lazy_load_if_possible("danfoss"), + lazy_load_if_possible("popp"), + lazy_load_if_possible("vimar"), + lazy_load_if_possible("resideo_korea"), + lazy_load_if_possible("aqara"), +} +return sub_drivers 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 84f305d428..e026edb5f8 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local clusters = require "st.zigbee.zcl.clusters" 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 ce735209c4..206333ffa7 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 51782ec8e5..98727e563e 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -104,4 +107,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 6ceb8fdc52..1f7a03f049 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 c5a820bdba..6e6cdb0305 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 628dea32eb..ecff7883da 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua @@ -1,3 +1,6 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + require "integration_test" -- Mock out globals local test = require "integration_test" 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 0d8365a521..4c7f4bdcaf 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 @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" 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 2411d7638c..6805e6c604 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 88810a3c80..5ca78c3675 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 9a5ba8aed5..0717fbb9fa 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d0951b2958..afb36720a3 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 ab7f4c19cc..3380856f1c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 d41d4f23aa..7b2a0cc8f6 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 bd16d17a62..638f3f7ff4 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" 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 01f1a0ba74..380eacc1ce 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua new file mode 100644 index 0000000000..3aa0478f46 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/vimar/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local vimar_thermostat_can_handle = function(opts, driver, device) + local VIMAR_THERMOSTAT_FINGERPRINT = { + mfr = "Vimar", + model = "WheelThermostat_v1.0" + } + + if device:get_manufacturer() == VIMAR_THERMOSTAT_FINGERPRINT.mfr and + device:get_model() == VIMAR_THERMOSTAT_FINGERPRINT.model then + return true, require("vimar") + end + return false +end + +return vimar_thermostat_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua index b4070cf569..7ad274449f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -38,11 +28,6 @@ local VIMAR_THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.HEAT] = ThermostatMode.thermostatMode.heat, } -local VIMAR_THERMOSTAT_FINGERPRINT = { - mfr = "Vimar", - model = "WheelThermostat_v1.0" -} - -- NOTE: This is a global variable to use in order to save the current thermostat profile local VIMAR_CURRENT_PROFILE = "_vimarThermostatCurrentProfile" @@ -50,10 +35,6 @@ local VIMAR_THERMOSTAT_HEATING_PROFILE = "thermostat-fanless-heating-no-fw" local VIMAR_THERMOSTAT_COOLING_PROFILE = "thermostat-fanless-cooling-no-fw" -local vimar_thermostat_can_handle = function(opts, driver, device) - return device:get_manufacturer() == VIMAR_THERMOSTAT_FINGERPRINT.mfr and - device:get_model() == VIMAR_THERMOSTAT_FINGERPRINT.model -end local vimar_thermostat_supported_modes_handler = function(driver, device, supported_modes) device:emit_event( @@ -182,7 +163,7 @@ local vimar_thermostat_subdriver = { } }, doConfigure = vimar_thermostat_do_configure, - can_handle = vimar_thermostat_can_handle + can_handle = require("vimar.can_handle"), } return vimar_thermostat_subdriver diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua new file mode 100644 index 0000000000..818764c88a --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function zenwithin_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Zen Within" and device:get_model() == "Zen-01" then + return true, require("zenwithin") + end + return false +end + +return zenwithin_can_handle diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua index e4b11de8be..1b71db2609 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local utils = require "st.utils" @@ -213,9 +203,7 @@ local zenwithin_thermostat = { infoChanged = info_changed, init = battery_defaults.build_linear_voltage_init(BAT_MIN, BAT_MAX) }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Zen Within" and device:get_model() == "Zen-01" - end + can_handle = require("zenwithin.can_handle"), } return zenwithin_thermostat From 9a714175757dea23539df997566506121325e578 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:27 -0600 Subject: [PATCH 423/449] CHAD-17092: zwave-sensor lazy loading of sub-drivers --- .../src/aeotec-multisensor/can_handle.lua | 15 ++++++ .../src/aeotec-multisensor/fingerprints.lua | 9 ++++ .../src/aeotec-multisensor/init.lua | 37 ++------------ .../multisensor-6/can_handle.lua | 11 ++++ .../aeotec-multisensor/multisensor-6/init.lua | 23 ++------- .../multisensor-7/can_handle.lua | 12 +++++ .../aeotec-multisensor/multisensor-7/init.lua | 24 ++------- .../src/aeotec-multisensor/sub_drivers.lua | 9 ++++ .../src/aeotec-water-sensor/can_handle.lua | 15 ++++++ .../src/aeotec-water-sensor/fingerprints.lua | 11 ++++ .../src/aeotec-water-sensor/init.lua | 34 ++----------- .../src/apiv6_bugfix/can_handle.lua | 35 +++++++++++++ .../zwave-sensor/src/apiv6_bugfix/init.lua | 33 ++---------- .../zwave-sensor/src/configurations.lua | 16 ++---- .../src/enerwave-motion-sensor/can_handle.lua | 12 +++++ .../src/enerwave-motion-sensor/init.lua | 27 ++-------- .../can_handle.lua | 17 +++++++ .../everspring-motion-light-sensor/init.lua | 32 ++---------- .../can_handle.lua | 15 ++++++ .../ezmultipli-multipurpose-sensor/init.lua | 32 +++--------- .../fibaro-door-window-sensor/can_handle.lua | 15 ++++++ .../can_handle.lua | 14 ++++++ .../fingerprints.lua | 8 +++ .../fibaro-door-window-sensor-1/init.lua | 30 ++--------- .../can_handle.lua | 14 ++++++ .../fingerprints.lua | 10 ++++ .../fibaro-door-window-sensor-2/init.lua | 32 ++---------- .../fingerprints.lua | 15 ++++++ .../src/fibaro-door-window-sensor/init.lua | 43 ++-------------- .../fibaro-door-window-sensor/sub_drivers.lua | 9 ++++ .../src/fibaro-flood-sensor/can_handle.lua | 14 ++++++ .../src/fibaro-flood-sensor/init.lua | 30 ++--------- .../src/fibaro-motion-sensor/can_handle.lua | 15 ++++++ .../src/fibaro-motion-sensor/init.lua | 29 ++--------- .../src/firmware-version/can_handle.lua | 21 ++++++++ .../src/firmware-version/init.lua | 34 ++----------- .../can_handle.lua | 20 ++++++++ .../glentronics-water-leak-sensor/init.lua | 36 ++----------- .../src/homeseer-multi-sensor/can_handle.lua | 20 ++++++++ .../src/homeseer-multi-sensor/init.lua | 36 ++----------- drivers/SmartThings/zwave-sensor/src/init.lua | 50 ++----------------- .../zwave-sensor/src/lazy_load_subdriver.lua | 18 +++++++ .../zwave-sensor/src/preferences.lua | 16 ++---- .../src/sensative-strip/can_handle.lua | 14 ++++++ .../zwave-sensor/src/sensative-strip/init.lua | 27 ++-------- .../zwave-sensor/src/sub_drivers.lua | 26 ++++++++++ .../src/test/test_aeon_multisensor.lua | 16 ++---- .../src/test/test_aeotec_multisensor_6.lua | 16 ++---- .../src/test/test_aeotec_multisensor_7.lua | 16 ++---- .../src/test/test_aeotec_multisensor_gen5.lua | 16 ++---- .../src/test/test_aeotec_water_sensor.lua | 16 ++---- .../src/test/test_aeotec_water_sensor_7.lua | 16 ++---- .../src/test/test_enerwave_motion_sensor.lua | 16 ++---- .../src/test/test_everpsring_sp817.lua | 16 ++---- .../src/test/test_everspring_PIR_sensor.lua | 16 ++---- .../src/test/test_everspring_ST814.lua | 16 ++---- .../test_everspring_illuminance_sensor.lua | 16 ++---- .../test_everspring_motion_light_sensor.lua | 16 ++---- .../test_ezmultipli_multipurpose_sensor.lua | 16 ++---- .../test/test_fibaro_door_window_sensor.lua | 16 ++---- .../test/test_fibaro_door_window_sensor_1.lua | 16 ++---- .../test/test_fibaro_door_window_sensor_2.lua | 16 ++---- ...ro_door_window_sensor_with_temperature.lua | 16 ++---- .../src/test/test_fibaro_flood_sensor.lua | 16 ++---- .../src/test/test_fibaro_flood_sensor_zw5.lua | 16 ++---- .../src/test/test_fibaro_motion_sensor.lua | 16 ++---- .../test/test_fibaro_motion_sensor_zw5.lua | 16 ++---- .../src/test/test_generic_sensor.lua | 16 ++---- .../test_glentronics_water_leak_sensor.lua | 16 ++---- .../src/test/test_homeseer_multi_sensor.lua | 16 ++---- .../src/test/test_no_wakeup_poll.lua | 18 ++----- .../src/test/test_sensative_strip.lua | 16 ++---- .../test_smartthings_water_leak_sensor.lua | 16 ++---- .../src/test/test_v1_contact_event.lua | 16 ++---- .../src/test/test_vision_motion_detector.lua | 16 ++---- .../src/test/test_zooz_4_in_1_sensor.lua | 16 ++---- .../test/test_zwave_motion_light_sensor.lua | 16 ++---- .../test_zwave_motion_temp_light_sensor.lua | 16 ++---- .../src/test/test_zwave_sensor.lua | 16 ++---- .../src/test/test_zwave_water_sensor.lua | 16 ++---- .../src/timed-tamper-clear/can_handle.lua | 23 +++++++++ .../src/timed-tamper-clear/init.lua | 36 ++----------- .../src/v1-contact-event/can_handle.lua | 22 ++++++++ .../src/v1-contact-event/init.lua | 31 ++---------- .../src/vision-motion-detector/can_handle.lua | 17 +++++++ .../src/vision-motion-detector/init.lua | 33 ++---------- .../src/wakeup-no-poll/can_handle.lua | 12 +++++ .../zwave-sensor/src/wakeup-no-poll/init.lua | 32 +++--------- .../src/zooz-4-in-1-sensor/can_handle.lua | 15 ++++++ .../src/zooz-4-in-1-sensor/fingerprints.lua | 10 ++++ .../src/zooz-4-in-1-sensor/init.lua | 33 ++---------- .../zwave-water-leak-sensor/can_handle.lua | 15 ++++++ .../zwave-water-leak-sensor/fingerprints.lua | 19 +++++++ .../src/zwave-water-leak-sensor/init.lua | 43 ++-------------- 94 files changed, 742 insertions(+), 1160 deletions(-) create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua new file mode 100644 index 0000000000..99cea3c0b3 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_aeotec_multisensor(opts, self, device, ...) + local FINGERPRINTS = require("aeotec-multisensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-multisensor") + return true, subdriver, require("aeotec-multisensor") + end + end + return false +end + +return can_handle_aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua new file mode 100644 index 0000000000..9436a85979 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local AEOTEC_MULTISENSOR_FINGERPRINTS = { + { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 + { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 +} + +return AEOTEC_MULTISENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua index edd01c7553..d1759a41e0 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,21 +7,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local AEOTEC_MULTISENSOR_FINGERPRINTS = { - { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 - { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 -} - -local function can_handle_aeotec_multisensor(opts, self, device, ...) - for _, fingerprint in ipairs(AEOTEC_MULTISENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-multisensor") - return true, subdriver - end - end - return false -end - local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -61,12 +35,9 @@ local aeotec_multisensor = { [Notification.REPORT] = notification_report_handler } }, - sub_drivers = { - require("aeotec-multisensor/multisensor-6"), - require("aeotec-multisensor/multisensor-7") - }, + sub_drivers = require("aeotec-multisensor.sub_drivers"), NAME = "aeotec multisensor", - can_handle = can_handle_aeotec_multisensor + can_handle = require("aeotec-multisensor.can_handle"), } return aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua new file mode 100644 index 0000000000..d86e9c8b3a --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multisensor_6(opts, self, device, ...) +local MULTISENSOR_6_PRODUCT_ID = 0x0064 + if device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID then + return true, require("aeotec-multisensor.multisensor-6") + end + return false +end +return can_handle_multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua index 1b9d4d6b97..4174b3b14e 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -19,12 +10,8 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version = 2}) -local MULTISENSOR_6_PRODUCT_ID = 0x0064 local PREFERENCE_NUM = 9 -local function can_handle_multisensor_6(opts, self, device, ...) - return device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID -end local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -62,7 +49,7 @@ local multisensor_6 = { } }, NAME = "aeotec multisensor 6", - can_handle = can_handle_multisensor_6 + can_handle = require("aeotec-multisensor.multisensor-6.can_handle"), } return multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua new file mode 100644 index 0000000000..f109d0e31c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_multisensor_7(opts, self, device, ...) + local MULTISENSOR_7_PRODUCT_ID = 0x0018 + if device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID then + return true, require("aeotec-multisensor.multisensor-7") + end + return false +end + +return can_handle_multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua index 2d2bf4e36e..c3dc69178f 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -19,13 +10,8 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 2 }) -local MULTISENSOR_7_PRODUCT_ID = 0x0018 local PREFERENCE_NUM = 10 -local function can_handle_multisensor_7(opts, self, device, ...) - return device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID -end - local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -62,7 +48,7 @@ local multisensor_7 = { } }, NAME = "aeotec multisensor 7", - can_handle = can_handle_multisensor_7 + can_handle = require("aeotec-multisensor.multisensor-7.can_handle"), } return multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua new file mode 100644 index 0000000000..396f53fe86 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("aeotec-multisensor/multisensor-6"), + lazy_load_if_possible("aeotec-multisensor/multisensor-7"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua new file mode 100644 index 0000000000..1bdf9f12a8 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("aeotec-water-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-water-sensor") + return true, subdriver, require("aeotec-water-sensor") + end + end + return false +end + +return can_handle_zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua new file mode 100644 index 0000000000..423d87754e --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU + { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US + { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU + { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 +} + +return ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua index 9d883ea3c2..4c7a86e708 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +9,8 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { - { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU - { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US - { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU - { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 -} --- Determine whether the passed device is zwave water temperature humidiry sensor -local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-water-sensor") - return true, subdriver - end - end - return false -end --- Default handler for notification command class reports --- @@ -68,7 +44,7 @@ local zwave_water_temp_humidity_sensor = { }, }, NAME = "zwave water temp humidity sensor", - can_handle = can_handle_zwave_water_temp_humidity_sensor + can_handle = require("aeotec-water-sensor.can_handle"), } return zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..4913e9a25e --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,35 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cc = require "st.zwave.CommandClass" +local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + +-- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll +local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor +local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat +local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 +local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 +local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor +local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 +local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} +local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, + AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, + ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} + +local function can_handle(opts, driver, device, cmd, ...) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + + for _, fp in ipairs(FPS) do + if device:id_match(fp.mfr, fp.prod, fp.model) then return false end + end + local subdriver = require("apiv6_bugfix") + return true, subdriver + else + return false + end +end + +return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua index 322333d565..94dc5975ab 100644 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua @@ -1,34 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) --- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll -local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor -local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat -local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 -local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 -local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor -local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 -local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} -local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, - AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, - ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - - for _, fp in ipairs(FPS) do - if device:id_match(fp.mfr, fp.prod, fp.model) then return false end - end - local subdriver = require("apiv6_bugfix") - return true, subdriver - else - return false - end -end - local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -40,7 +15,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-sensor/src/configurations.lua b/drivers/SmartThings/zwave-sensor/src/configurations.lua index 2883e70384..0a3c62ead8 100644 --- a/drivers/SmartThings/zwave-sensor/src/configurations.lua +++ b/drivers/SmartThings/zwave-sensor/src/configurations.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Configuration diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..a707d7493a --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) + local ENERWAVE_MFR = 0x011A + if device.zwave_manufacturer_id == ENERWAVE_MFR then + local subdriver = require("enerwave-motion-sensor") + return true, subdriver, require("enerwave-motion-sensor") + else return false end +end + +return can_handle_enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua index 6fb712e3b0..012a8884a3 100644 --- a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,15 +10,6 @@ local Association = (require "st.zwave.CommandClass.Association")({version=2}) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) -local ENERWAVE_MFR = 0x011A - -local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) - if device.zwave_manufacturer_id == ENERWAVE_MFR then - local subdriver = require("enerwave-motion-sensor") - return true, subdriver - else return false end -end - local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -58,7 +39,7 @@ local enerwave_motion_sensor = { doConfigure = do_configure }, NAME = "enerwave_motion_sensor", - can_handle = can_handle_enerwave_motion_sensor + can_handle = require("enerwave-motion-sensor.can_handle") } return enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua new file mode 100644 index 0000000000..c9fd2eafd1 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_everspring_motion_light(opts, driver, device, ...) + local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } + if device:id_match( + EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.model + ) then + local subdriver = require("everspring-motion-light-sensor") + return true, subdriver + end + return false +end + +return can_handle_everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua index 1b11aadabe..8baa3756d6 100644 --- a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua @@ -1,34 +1,12 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2,strict=true}) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version=2}) -local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } - -local function can_handle_everspring_motion_light(opts, driver, device, ...) - if device:id_match( - EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.model - ) then - local subdriver = require("everspring-motion-light-sensor") - return true, subdriver - else return false end -end - local function device_added(driver, device) device:emit_event(capabilities.motionSensor.motion.inactive()) device:send(SwitchBinary:Get({})) @@ -40,7 +18,7 @@ local everspring_motion_light = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_everspring_motion_light + can_handle = require("everspring-motion-light-sensor.can_handle"), } return everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua new file mode 100644 index 0000000000..5596894fbb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) + local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } + if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then + local subdriver = require("ezmultipli-multipurpose-sensor") + return true, subdriver + end + return false +end + +return can_handle_ezmultipli_multipurpose_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua index 1e4b3bf0ce..f4cac30faa 100644 --- a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.utils @@ -28,17 +19,6 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) local CAP_CACHE_KEY = "st.capabilities." .. capabilities.colorControl.ID -local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } - -local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) - if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("ezmultipli-multipurpose-sensor") - return true, subdriver - else return false end -end - local function basic_report_handler(driver, device, cmd) local event local value = (cmd.args.target_value ~= nil) and cmd.args.target_value or cmd.args.value @@ -102,7 +82,7 @@ local ezmultipli_multipurpose_sensor = { [capabilities.colorControl.commands.setColor.NAME] = set_color } }, - can_handle = can_handle_ezmultipli_multipurpose_sensor + can_handle = require("ezmultipli-multipurpose-sensor.can_handle"), } -return ezmultipli_multipurpose_sensor \ No newline at end of file +return ezmultipli_multipurpose_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua new file mode 100644 index 0000000000..c088fd2b2f --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then + local subdriver = require("fibaro-door-window-sensor") + return true, subdriver, require("fibaro-door-window-sensor") + end + end + return false +end + +return can_handle_fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua new file mode 100644 index 0000000000..992ea8fd9d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-1") + end + end + return false +end + +return can_handle_fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua new file mode 100644 index 0000000000..50727133bb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } +} + +return FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua index 698fffcceb..4dbca58919 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -21,19 +10,6 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 1 }) local configurationsMap = require "configurations" -local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -} - -local function can_handle_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end - local function sensor_alarm_report_handler(driver, device, cmd) if (cmd.args.sensor_state == SensorAlarm.sensor_state.ALARM) then device:emit_event(capabilities.tamperAlert.tamper.detected()) @@ -92,7 +68,7 @@ local fibaro_door_window_sensor_1 = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh }, - can_handle = can_handle_fibaro_door_window_sensor_1 + can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.can_handle"), } return fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua new file mode 100644 index 0000000000..4493496f94 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-2") + end + end + return false +end + +return can_handle_fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua new file mode 100644 index 0000000000..6103c107d1 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ +} + +return FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua index 16c5ec2017..250203b0cd 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,21 +7,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) -local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ -} - -local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end - local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) if device:get_latest_state(component, capability.ID, attribute_name) == nil then device:emit_event(value) @@ -83,7 +57,7 @@ local fibaro_door_window_sensor_2 = { lifecycle_handlers = { added = device_added }, - can_handle = can_handle_fibaro_door_window_sensor_2, + can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.can_handle"), } return fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua new file mode 100644 index 0000000000..699df3f623 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor +} + +return FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua index 86cf865348..6c30508fd1 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -24,27 +13,6 @@ local preferencesMap = require "preferences" local FIBARO_DOOR_WINDOW_SENSOR_WAKEUP_INTERVAL = 21600 --seconds -local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor -} - -local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then - local subdriver = require("fibaro-door-window-sensor") - return true, subdriver - end - end - return false -end - local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do if parameter.parameter_number == parameterNumber then @@ -154,11 +122,8 @@ local fibaro_door_window_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - sub_drivers = { - require("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), - require("fibaro-door-window-sensor/fibaro-door-window-sensor-2") - }, - can_handle = can_handle_fibaro_door_window_sensor + sub_drivers = require("fibaro-door-window-sensor.sub_drivers"), + can_handle = require("fibaro-door-window-sensor.can_handle"), } return fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua new file mode 100644 index 0000000000..0c4ddd4e43 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), + lazy_load_if_possible("fibaro-door-window-sensor/fibaro-door-window-sensor-2"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua new file mode 100644 index 0000000000..341fbcd6f9 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_flood_sensor(opts, driver, device, ...) + local FIBARO_MFR_ID = 0x010F + local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } + if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then + local subdriver = require("fibaro-flood-sensor") + return true, subdriver + end + return false +end + +return can_handle_fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index 144be985ae..9a0a8e7eaa 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -26,17 +17,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = local preferences = require "preferences" local configurations = require "configurations" -local FIBARO_MFR_ID = 0x010F -local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } - -local function can_handle_fibaro_flood_sensor(opts, driver, device, ...) - if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then - local subdriver = require("fibaro-flood-sensor") - return true, subdriver - else return false end -end - - local function basic_set_handler(self, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -96,7 +76,7 @@ local fibaro_flood_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = can_handle_fibaro_flood_sensor + can_handle = require("fibaro-flood-sensor.can_handle"), } return fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua new file mode 100644 index 0000000000..cd3f647394 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) + + local FIBARO_MOTION_MFR = 0x010F + local FIBARO_MOTION_PROD = 0x0800 + if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then + local subdriver = require("fibaro-motion-sensor") + return true, subdriver, require("fibaro-motion-sensor") + end + return false +end + +return can_handle_fibaro_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua index ae45f5a27b..ed035bde18 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -18,15 +8,6 @@ local cc = require "st.zwave.CommandClass" local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 }) local capabilities = require "st.capabilities" -local FIBARO_MOTION_MFR = 0x010F -local FIBARO_MOTION_PROD = 0x0800 - -local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) - if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then - local subdriver = require("fibaro-motion-sensor") - return true, subdriver - else return false end -end local function sensor_alarm_report(driver, device, cmd) if (cmd.args.sensor_state ~= SensorAlarm.sensor_state.NO_ALARM) then @@ -43,7 +24,7 @@ local fibaro_motion_sensor = { [SensorAlarm.REPORT] = sensor_alarm_report } }, - can_handle = can_handle_fibaro_motion_sensor + can_handle = require("fibaro-motion-sensor.can_handle") } -return fibaro_motion_sensor \ No newline at end of file +return fibaro_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua new file mode 100644 index 0000000000..3ecdc2baf0 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +--This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled +local FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor +} + +return function(opts, driver, device, ...) + if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subDriver = require("firmware-version") + return true, subDriver + end + end + end + return false +end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua index 058a7f955c..528cf7da45 100644 --- a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,22 +10,6 @@ local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) ---This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled -local FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor -} - -local function can_handle_fw(opts, driver, device, ...) - if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subDriver = require("firmware-version") - return true, subDriver - end - end - end - return false -end --Runs upstream handlers (ex zwave_handlers) local function call_parent_handler(handlers, self, device, event, args) @@ -73,7 +47,7 @@ end local firmware_version = { NAME = "firmware_version", - can_handle = can_handle_fw, + can_handle = require("firmware-version.can_handle"), lifecycle_handlers = { added = added_handler, diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua new file mode 100644 index 0000000000..e24d7b9cf2 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is glentronics water leak sensor +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) + local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor + if device:id_match( + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then + return true, require("glentronics-water-leak-sensor") + end + return false +end + +return can_handle_glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua index 3dba7351d6..7400d889b7 100644 --- a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -18,23 +9,6 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor - ---- Determine whether the passed device is glentronics water leak sensor ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) - if device:id_match( - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("glentronics-water-leak-sensor") - return true, subdriver - else return false end -end - local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -78,7 +52,7 @@ local glentronics_water_leak_sensor = { added = device_added }, NAME = "glentronics water leak sensor", - can_handle = can_handle_glentronics_water_leak_sensor + can_handle = require("glentronics-water-leak-sensor.can_handle"), } return glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua new file mode 100644 index 0000000000..992c1f7c7f --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua @@ -0,0 +1,20 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is homeseer multi sensor +--- +--- @param driver Driver driver instance +--- @param device Device device instance +--- @return boolean true if the device proper, else false +local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) + local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 + if device:id_match( + HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then + return true, require("homeseer-multi-sensor") + end + return false +end + +return can_handle_homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua index 2330f28106..f89e6a7870 100644 --- a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,23 +13,6 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version = 5}) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1}) -local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 - ---- Determine whether the passed device is homeseer multi sensor ---- ---- @param driver Driver driver instance ---- @param device Device device instance ---- @return boolean true if the device proper, else false -local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) - if device:id_match( - HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("homeseer-multi-sensor") - return true, subdriver - else return false end -end - local function basic_set_handler(self, device, cmd) if cmd.args.value ~= nil then device:emit_event(cmd.args.value == 0xFF and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -87,7 +61,7 @@ local homeseer_multi_sensor = { init = device_init, }, NAME = "homeseer multi sensor", - can_handle = can_handle_homeseer_multi_sensor + can_handle = require("homeseer-multi-sensor.can_handle"), } return homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 213aa8c389..2c18e4c2ed 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -27,19 +16,6 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local preferences = require "preferences" local configurations = require "configurations" -local function lazy_load_if_possible(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - - -- version 9 will include the lazy loading functions - if version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end - --- Handle preference changes --- --- @param driver st.zwave.Driver @@ -134,27 +110,7 @@ local driver_template = { capabilities.powerMeter, capabilities.smokeDetector }, - sub_drivers = { - lazy_load_if_possible("zooz-4-in-1-sensor"), - lazy_load_if_possible("vision-motion-detector"), - lazy_load_if_possible("fibaro-flood-sensor"), - lazy_load_if_possible("aeotec-water-sensor"), - lazy_load_if_possible("glentronics-water-leak-sensor"), - lazy_load_if_possible("homeseer-multi-sensor"), - lazy_load_if_possible("fibaro-door-window-sensor"), - lazy_load_if_possible("sensative-strip"), - lazy_load_if_possible("enerwave-motion-sensor"), - lazy_load_if_possible("aeotec-multisensor"), - lazy_load_if_possible("zwave-water-leak-sensor"), - lazy_load_if_possible("everspring-motion-light-sensor"), - lazy_load_if_possible("ezmultipli-multipurpose-sensor"), - lazy_load_if_possible("fibaro-motion-sensor"), - lazy_load_if_possible("v1-contact-event"), - lazy_load_if_possible("timed-tamper-clear"), - lazy_load_if_possible("wakeup-no-poll"), - lazy_load_if_possible("firmware-version"), - lazy_load_if_possible("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { added = added_handler, init = device_init, diff --git a/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 9585b6ffe9..70293b10fa 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua new file mode 100644 index 0000000000..e639c32c94 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_sensative_strip(opts, driver, device, cmd, ...) + local SENSATIVE_MFR = 0x019A + local SENSATIVE_MODEL = 0x000A + if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then + local subdriver = require("sensative-strip") + return true, subdriver, require("sensative-strip") + end + return false +end + +return can_handle_sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua index 73f1cb8459..7fd9fb2258 100644 --- a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -19,20 +9,11 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) -local SENSATIVE_MFR = 0x019A -local SENSATIVE_MODEL = 0x000A local LEAKAGE_ALARM_PARAM = 12 local LEAKAGE_ALARM_OFF = 0 local SENSATIVE_COMFORT_PROFILE = "illuminance-temperature" local CONFIG_REPORT_RECEIVED = "configReportReceived" -local function can_handle_sensative_strip(opts, driver, device, cmd, ...) - if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then - local subdriver = require("sensative-strip") - return true, subdriver - else return false end -end - local function configuration_report(driver, device, cmd) local parameter_number = cmd.args.parameter_number local configuration_value = cmd.args.configuration_value @@ -75,7 +56,7 @@ local sensative_strip = { doConfigure = do_configure }, NAME = "sensative_strip", - can_handle = can_handle_sensative_strip + can_handle = require("sensative-strip.can_handle") } return sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua new file mode 100644 index 0000000000..9504479304 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua @@ -0,0 +1,26 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("zooz-4-in-1-sensor"), + lazy_load_if_possible("vision-motion-detector"), + lazy_load_if_possible("fibaro-flood-sensor"), + lazy_load_if_possible("aeotec-water-sensor"), + lazy_load_if_possible("glentronics-water-leak-sensor"), + lazy_load_if_possible("homeseer-multi-sensor"), + lazy_load_if_possible("fibaro-door-window-sensor"), + lazy_load_if_possible("sensative-strip"), + lazy_load_if_possible("enerwave-motion-sensor"), + lazy_load_if_possible("aeotec-multisensor"), + lazy_load_if_possible("zwave-water-leak-sensor"), + lazy_load_if_possible("everspring-motion-light-sensor"), + lazy_load_if_possible("ezmultipli-multipurpose-sensor"), + lazy_load_if_possible("fibaro-motion-sensor"), + lazy_load_if_possible("v1-contact-event"), + lazy_load_if_possible("timed-tamper-clear"), + lazy_load_if_possible("wakeup-no-poll"), + lazy_load_if_possible("firmware-version"), + lazy_load_if_possible("apiv6_bugfix"), +} 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 2d83fd6e25..cfdb08f89d 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 02c2adf751..e78e63e214 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 7fd57e42b2..5bbd1b4f0f 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 3b73ff17c3..57584e779c 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 8af011f51f..e58badc224 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 a5f44ff12e..eb951cef9b 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 a90655669d..bf2d800817 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 45e0672c11..87d87b75b5 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 1cb2735a31..c6192bcddf 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 097016fc81..c576543902 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 04103db0db..f6db164cfe 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 17a627a736..6e04140010 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 74b6d6196c..2e0ead247a 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 248c22ab60..d3f51b0bb5 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 6904f318a9..d597b72bdc 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 bc78bd02ca..2bbe391a62 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 2b27d66868..0fd2d2c8df 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 7a9743a69e..98edcbf9f6 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 45cde29b8f..0ad90d3479 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 24a7f31eaf..6d1f459361 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 e514a2f2e5..4b322b315d 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 643c69dd1f..24524d9106 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 747c4cbce1..5ba9ee537e 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 f57bfe7950..52a273f0b7 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 9818f1422c..65f30fb7a8 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" @@ -113,4 +103,4 @@ test.register_message_test( } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 873909df23..a5be01cb81 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zw = require "st.zwave" 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 2321b5bd09..1b89db9f9f 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 866508e3d6..b4de025737 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 @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 e1b82a6b38..1cf3da742a 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 1bef24c5d6..c0cc183c41 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 4ff11090c9..4b149c9be7 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 d918f47179..2256522888 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 a390c35db9..3719a31f1c 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 e6caf1f07c..2a8eea8eab 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua new file mode 100644 index 0000000000..c05cbdcf7d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua @@ -0,0 +1,23 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_tamper_event(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) + local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F + + if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and + opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or + cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then + return true, require("timed-tamper-clear") + end + return false +end + +return can_handle_tamper_event 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 2007bedb0d..c2420791b5 100644 --- a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua @@ -1,16 +1,7 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -20,23 +11,6 @@ local capabilities = require "st.capabilities" local TAMPER_TIMER = "_tamper_timer" local TAMPER_CLEAR = 10 -local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F - -local function can_handle_tamper_event(opts, driver, device, cmd, ...) - if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and - opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or - cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then - local subdriver = require("timed-tamper-clear") - return true, subdriver - else return false - end -end -- This behavior is from zwave-door-window-sensor.groovy. We've seen this behavior -- in Ecolink and several other z-wave sensors that do not send tamper clear events @@ -60,7 +34,7 @@ local timed_tamper_clear = { } }, NAME = "timed tamper clear", - can_handle = can_handle_tamper_event + can_handle = require("timed-tamper-clear.can_handle"), } return timed_tamper_clear diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua new file mode 100644 index 0000000000..2fa45d8015 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua @@ -0,0 +1,22 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) + + if opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + cmd.args.v1_alarm_type == 0x07 then + local subdriver = require("v1-contact-event") + return true, subdriver, require("v1-contact-event") + else + return false + end +end + +return can_handle_v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua index 40efc7633e..887ac32bf0 100644 --- a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -18,20 +7,6 @@ local cc = require "st.zwave.CommandClass" local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) local capabilities = require "st.capabilities" -local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) - if opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - cmd.args.v1_alarm_type == 0x07 then - local subdriver = require("v1-contact-event") - return true, subdriver - else - return false - end -end -- This behavior is from zwave-door-window-sensor.groovy, where it is -- indicated that certain monoprice sensors had this behavior. Also, @@ -53,7 +28,7 @@ local v1_contact_event = { } }, NAME = "v1 contact event", - can_handle = can_handle_v1_contact_event + can_handle = require("v1-contact-event.can_handle"), } return v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua new file mode 100644 index 0000000000..d270a7954d --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +--- Determine whether the passed device is zwave-plus-motion-temp-sensor +local function can_handle_vision_motion_detector(opts, driver, device, ...) + local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 + if device:id_match( + VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, + VISION_MOTION_DETECTOR_FINGERPRINTS.productType, + VISION_MOTION_DETECTOR_FINGERPRINTS.productId + ) then + return true, require("vision-motion-detector") + end + return false +end + +return can_handle_vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua index 320bf3824f..72934362c5 100644 --- a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,20 +13,6 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) -local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 - ---- Determine whether the passed device is zwave-plus-motion-temp-sensor -local function can_handle_vision_motion_detector(opts, driver, device, ...) - if device:id_match( - VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, - VISION_MOTION_DETECTOR_FINGERPRINTS.productType, - VISION_MOTION_DETECTOR_FINGERPRINTS.productId - ) then - local subdriver = require("vision-motion-detector") - return true, subdriver - else return false end -end - --- Handler for notification report command class from sensor --- --- @param self st.zwave.Driver @@ -83,7 +60,7 @@ local vision_motion_detector = { doConfigure = do_configure, }, NAME = "Vision motion detector", - can_handle = can_handle_vision_motion_detector + can_handle = require("vision-motion-detector.can_handle"), } return vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua new file mode 100644 index 0000000000..15ac66d439 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, ...) + local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("wakeup-no-poll") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua index 59d298a0e4..1270cd4557 100644 --- a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua @@ -1,16 +1,7 @@ --- Copyright 2023 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. +-- Copyright 2023 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" @@ -21,17 +12,6 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version = 2 --- @type st.zwave.CommandClass.Battery local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor - -local function can_handle(opts, driver, device, ...) - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("wakeup-no-poll") - return true, subdriver - else - return false - end -end - -- Nortek open/closed sensors _always_ respond with "open" when polled, and they are polled after wakeup local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -53,7 +33,7 @@ local wakeup_no_poll = { [WakeUp.NOTIFICATION] = wakeup_notification } }, - can_handle = can_handle + can_handle = require("wakeup-no-poll.can_handle"), } -return wakeup_no_poll \ No newline at end of file +return wakeup_no_poll diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua new file mode 100644 index 0000000000..17c80b70fb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zooz-4-in-1-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("zooz-4-in-1-sensor") + return true, subdriver, require("zooz-4-in-1-sensor") + end + end + return false +end + +return can_handle_zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua new file mode 100644 index 0000000000..12d853b147 --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZOOZ_4_IN_1_FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor + { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion + { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 +} + +return ZOOZ_4_IN_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua index 5d4570e525..7321c3f9b4 100644 --- a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -22,22 +13,8 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.utils local utils = require "st.utils" -local ZOOZ_4_IN_1_FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor - { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion - { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 -} --- Determine whether the passed device is zooz_4_in_1_sensor -local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(ZOOZ_4_IN_1_FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("zooz-4-in-1-sensor") - return true, subdriver - end - end - return false -end --- Handler for notification report command class --- @@ -109,7 +86,7 @@ local zooz_4_in_1_sensor = { } }, NAME = "zooz 4 in 1 sensor", - can_handle = can_handle_zooz_4_in_1_sensor + can_handle = require("zooz-4-in-1-sensor.can_handle"), } return zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua new file mode 100644 index 0000000000..ad7e736a7c --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_water_leak_sensor(opts, driver, device, ...) + local FINGERPRINTS = require("zwave-water-leak-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zwave-water-leak-sensor") + return true, subdriver, require("zwave-water-leak-sensor") + end + end + return false +end + +return can_handle_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua new file mode 100644 index 0000000000..c07bdb52cb --- /dev/null +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua @@ -0,0 +1,19 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local WATER_LEAK_SENSOR_FINGERPRINTS = { + {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor + {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor + {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor + {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) + {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) + {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) + {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor + {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector + {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor +} + +return WATER_LEAK_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua index 1eefab7479..4575de352a 100644 --- a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua @@ -1,47 +1,10 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) - -local WATER_LEAK_SENSOR_FINGERPRINTS = { - {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor - {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor - {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor - {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) - {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) - {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) - {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor - {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector - {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor -} - -local function can_handle_water_leak_sensor(opts, driver, device, ...) - for _, fingerprint in ipairs(WATER_LEAK_SENSOR_FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zwave-water-leak-sensor") - return true, subdriver - end - end - return false -end - local function basic_set_handler(driver, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -54,7 +17,7 @@ local water_leak_sensor = { [Basic.SET] = basic_set_handler } }, - can_handle = can_handle_water_leak_sensor + can_handle = require("zwave-water-leak-sensor.can_handle"), } return water_leak_sensor From 13c64b210b78edd9c527ae038147eff6366c2d07 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Thu, 12 Feb 2026 13:10:33 -0600 Subject: [PATCH 424/449] Fixup tests for new native handlers in 60 --- .../src/test/test_all_capability_zigbee_bulb.lua | 16 ++++++++++++++++ .../zigbee-switch/src/test/test_zll_rgb_bulb.lua | 2 ++ .../src/test/test_generic_sensor.lua | 8 ++++++++ 3 files changed, 26 insertions(+) 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 70dfc96780..b581c5c61e 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 @@ -233,6 +233,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.hue(0)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "hue" } + } } } ) @@ -250,6 +258,14 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(50)) + }, + { + channel = "devices", + direction = "send", + message = { + "register_native_capability_attr_handler", + { device_uuid = mock_device.id, capability_id = "colorControl", capability_attr_id = "saturation" } + } } } ) 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 3435b22288..a8384d1b13 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 @@ -205,6 +205,7 @@ for _, data in ipairs(test_data) do if data.saturation ~= nil then test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.CurrentSaturation:build_test_attr_report(mock_device, math.ceil(data.saturation / 100 * 0xFE))}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(data.saturation))) + mock_device:expect_native_attr_handler_registration("colorControl", "saturation") end test.timer.__create_and_queue_test_time_advance_timer(0.2, "oneshot") @@ -249,6 +250,7 @@ for _, data in ipairs(test_data) do if data.hue ~= nil then test.socket.zigbee:__queue_receive({mock_device.id, ColorControl.attributes.CurrentHue:build_test_attr_report(mock_device, math.ceil(data.hue / 100 * 0xFE))}) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.hue(data.hue))) + mock_device:expect_native_attr_handler_registration("colorControl", "hue") end test.timer.__create_and_queue_test_time_advance_timer(0.2, "oneshot") 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 643c69dd1f..e6fff57b0d 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -1548,6 +1548,14 @@ test.register_message_test( mock_device, Meter:Get({scale = 0}) ) + }, + { + channel = "zwave", + direction = "send", + message = zw_test_utils.zwave_test_build_send_command( + mock_device, + Meter:Get({scale = 4}) + ) } }, { From 6863a78c336c9d5fb3eac6713db25202b81749a3 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Tue, 16 Dec 2025 13:06:17 -0600 Subject: [PATCH 425/449] add statelessStep capability support --- .../light-color-level-1800K-6500K.yml | 4 + .../light-color-level-2000K-7000K.yml | 4 + .../light-color-level-2200K-6500K.yml | 4 + .../light-color-level-2700K-6500K.yml | 4 + .../profiles/light-color-level-fan.yml | 4 + ...-level-illuminance-motion-1000K-15000K.yml | 4 + .../light-color-level-illuminance-motion.yml | 4 + .../profiles/light-color-level.yml | 4 + .../profiles/light-level-2-button.yml | 2 + .../profiles/light-level-3-button.yml | 2 + .../profiles/light-level-4-button.yml | 2 + .../profiles/light-level-5-button.yml | 2 + .../profiles/light-level-6-button.yml | 2 + .../profiles/light-level-7-button.yml | 2 + .../profiles/light-level-8-button.yml | 2 + ...ight-level-ColorTemperature-1500-9000k.yml | 4 + .../profiles/light-level-button.yml | 2 + ...ght-level-colorTemperature-2200K-6500K.yml | 4 + ...ght-level-colorTemperature-2700K-6500K.yml | 4 + ...ght-level-colorTemperature-2710k-6500k.yml | 4 + .../profiles/light-level-colorTemperature.yml | 4 + .../light-level-energy-powerConsumption.yml | 2 + .../profiles/light-level-motion.yml | 2 + ...ht-level-power-energy-powerConsumption.yml | 2 + .../profiles/light-level-power.yml | 2 + .../matter-switch/profiles/light-level.yml | 2 + .../plug-level-energy-powerConsumption.yml | 2 + ...ug-level-power-energy-powerConsumption.yml | 2 + .../profiles/plug-level-power.yml | 2 + .../matter-switch/profiles/plug-level.yml | 2 + .../profiles/switch-color-level.yml | 4 + .../switch-level-colorTemperature.yml | 4 + .../matter-switch/profiles/switch-level.yml | 2 + .../SmartThings/matter-switch/src/init.lua | 6 + .../switch_handlers/capability_handlers.lua | 36 +++- .../matter-switch/src/switch_utils/fields.lua | 17 +- .../test/test_light_illuminance_motion.lua | 10 +- .../src/test/test_matter_light_fan.lua | 4 +- .../test_matter_multi_button_switch_mcd.lua | 4 +- .../src/test/test_matter_switch.lua | 18 +- .../test_multi_switch_parent_child_lights.lua | 6 +- .../test_multi_switch_parent_child_plugs.lua | 6 +- .../src/test/test_stateless_step.lua | 163 ++++++++++++++++++ 43 files changed, 331 insertions(+), 35 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml index 58c8e0fca6..a0f5196b12 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-1800K-6500K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1800, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml index 4772e22f66..7424d241e2 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2000K-7000K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2000, 7000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml index 4977423135..a098cdd06f 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2200K-6500K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml index dbef511bbb..1b469ae8e8 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-2700K-6500K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml index 2fabc23bd7..2f91bcb04e 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-fan.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: fanMode diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml index dd48e4e81a..fb7a4fca94 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion-1000K-15000K.yml @@ -7,12 +7,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1000, 15000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: motionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml index 999e64a047..50f68f60fc 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level-illuminance-motion.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: motionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/light-color-level.yml b/drivers/SmartThings/matter-switch/profiles/light-color-level.yml index 572686ffdd..2f0f673bee 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-color-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-color-level.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml index 7c8b60ef56..6a4fbcc9d0 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-2-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml index 59600efd72..9e3106d9de 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-3-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml index b49b7f2254..43bcec5955 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-4-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml index ee55a6a394..2b98b9784d 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-5-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml index 805c97763e..230b2ea341 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-6-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml index 5cd1666a5f..13b3ee9443 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-7-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml index 4636359e92..a2dc732257 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-8-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml index f19e55ec9c..effe5f93b6 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-ColorTemperature-1500-9000k.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 1500, 9000 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml index 9fc53f642b..8334e32feb 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-button.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-button.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml index 79d6556485..2541fddc09 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2200K-6500K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml index 75b2bd488c..7c845e8238 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2700K-6500K.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2700, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml index b49179036f..5ccc67ab4c 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature-2710k-6500k.yml @@ -11,12 +11,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2710, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml index 4d130281d7..5681a97e9e 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-colorTemperature.yml @@ -10,12 +10,16 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml index 03963ccbd2..b2ddcc0a60 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-energy-powerConsumption.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: energyMeter version: 1 - id: powerConsumptionReport diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml b/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml index bdea457c21..54ce30abdf 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-motion.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: motionSensor version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml index f6c45ed1f7..31c6d44ee1 100755 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power-energy-powerConsumption.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/matter-switch/profiles/light-level-power.yml b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml index 23625ada16..86d61c0345 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level-power.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level-power.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/light-level.yml b/drivers/SmartThings/matter-switch/profiles/light-level.yml index e266f497c9..3b286f8262 100644 --- a/drivers/SmartThings/matter-switch/profiles/light-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/light-level.yml @@ -10,6 +10,8 @@ components: values: - key: "level.value" range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml index 86bd861bce..4ae29d2320 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-energy-powerConsumption.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: energyMeter version: 1 - id: powerConsumptionReport diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml index 17e7d6b7a0..477fb8b769 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-power-energy-powerConsumption.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: energyMeter diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml b/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml index d175930a92..8bb1c56361 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level-power.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: powerMeter version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/plug-level.yml b/drivers/SmartThings/matter-switch/profiles/plug-level.yml index 0d888b843f..90fa2ece47 100644 --- a/drivers/SmartThings/matter-switch/profiles/plug-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/plug-level.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml index f1f9e78438..c684f632c9 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-color-level.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: colorControl version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml b/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml index 42e3ef6257..9cba419d02 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-level-colorTemperature.yml @@ -6,12 +6,16 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: colorTemperature version: 1 config: values: - key: "colorTemperature.value" range: [ 2200, 6500 ] + - id: statelessColorTemperatureStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/profiles/switch-level.yml b/drivers/SmartThings/matter-switch/profiles/switch-level.yml index 8f3b9f5e5c..827fcdd898 100644 --- a/drivers/SmartThings/matter-switch/profiles/switch-level.yml +++ b/drivers/SmartThings/matter-switch/profiles/switch-level.yml @@ -6,6 +6,8 @@ components: version: 1 - id: switchLevel version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index e8d1b8330a..cac42e4483 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -298,6 +298,12 @@ local matter_driver_template = { [capabilities.level.ID] = { [capabilities.level.commands.setLevel.NAME] = capability_handlers.handle_set_level }, + [capabilities.statelessColorTemperatureStep.ID] = { + [capabilities.statelessColorTemperatureStep.commands.stepColorTemperatureByPercent.NAME] = capability_handlers.handle_step_color_temperature_by_percent, + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = capability_handlers.handle_step_level, + }, [capabilities.switch.ID] = { [capabilities.switch.commands.off.NAME] = capability_handlers.handle_switch_off, [capabilities.switch.commands.on.NAME] = capability_handlers.handle_switch_on, diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua index f96686b73a..3c65e3e8c0 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/capability_handlers.lua @@ -47,6 +47,17 @@ function CapabilityHandlers.handle_switch_set_level(driver, device, cmd) end +-- [[ STATELESS SWITCH LEVEL STEP CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_step_level(driver, device, cmd) + local step_size = st_utils.round((cmd.args and cmd.args.stepSize or 0)/100.0 * 254) + if step_size == 0 then return end + local endpoint_id = device:component_to_endpoint(cmd.component) + local step_mode = step_size > 0 and clusters.LevelControl.types.StepMode.UP or clusters.LevelControl.types.StepMode.DOWN + device:send(clusters.LevelControl.server.commands.Step(device, endpoint_id, step_mode, math.abs(step_size), fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) +end + + -- [[ COLOR CONTROL CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_set_color(driver, device, cmd) @@ -56,10 +67,10 @@ function CapabilityHandlers.handle_set_color(driver, device, cmd) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.color.hue) local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + req = clusters.ColorControl.server.commands.MoveToHueAndSaturation(device, endpoint_id, hue, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) else local x, y, _ = st_utils.safe_hsv_to_xy(cmd.args.color.hue, cmd.args.color.saturation) - req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + req = clusters.ColorControl.server.commands.MoveToColor(device, endpoint_id, x, y, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) end device:send(req) end @@ -69,7 +80,7 @@ function CapabilityHandlers.handle_set_hue(driver, device, cmd) local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local hue = switch_utils.convert_huesat_st_to_matter(cmd.args.hue) - local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + local req = clusters.ColorControl.server.commands.MoveToHue(device, endpoint_id, hue, 0, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) device:send(req) else device.log.warn("Device does not support huesat features on its color control cluster") @@ -81,7 +92,7 @@ function CapabilityHandlers.handle_set_saturation(driver, device, cmd) local huesat_endpoints = device:get_endpoints(clusters.ColorControl.ID, {feature_bitmap = clusters.ColorControl.FeatureMap.HUE_AND_SATURATION}) if switch_utils.tbl_contains(huesat_endpoints, endpoint_id) then local sat = switch_utils.convert_huesat_st_to_matter(cmd.args.saturation) - local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + local req = clusters.ColorControl.server.commands.MoveToSaturation(device, endpoint_id, sat, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) device:send(req) else device.log.warn("Device does not support huesat features on its color control cluster") @@ -103,12 +114,27 @@ function CapabilityHandlers.handle_set_color_temperature(driver, device, cmd) elseif max_temp_kelvin ~= nil and temp_in_kelvin >= max_temp_kelvin then temp_in_mired = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) end - local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.OPTIONS_OVERRIDE) + local req = clusters.ColorControl.server.commands.MoveToColorTemperature(device, endpoint_id, temp_in_mired, fields.TRANSITION_TIME, fields.OPTIONS_MASK, fields.HANDLE_COMMAND_IF_OFF) device:set_field(fields.MOST_RECENT_TEMP, cmd.args.temperature, {persist = true}) device:send(req) end +-- [[ STATELESS COLOR TEMPERATURE STEP CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.handle_step_color_temperature_by_percent(driver, device, cmd) + local step_percent_change = cmd.args and cmd.args.stepSize or 0 + if step_percent_change == 0 then return end + local endpoint_id = device:component_to_endpoint(cmd.component) + -- before the Matter 1.3 lua libs update (HUB FW 55), there was no ColorControl StepModeEnum type defined + local step_mode = step_percent_change > 0 and (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.DOWN or 3) or (clusters.ColorControl.types.StepModeEnum and clusters.ColorControl.types.StepModeEnum.UP or 1) + local min_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MIN, endpoint_id) or fields.COLOR_TEMPERATURE_MIRED_MIN -- default min mireds + local max_mireds = switch_utils.get_field_for_endpoint(device, fields.COLOR_TEMP_BOUND_RECEIVED_MIRED..fields.COLOR_TEMP_MAX, endpoint_id) or fields.COLOR_TEMPERATURE_MIRED_MAX -- default max mireds + local step_size_in_mireds = st_utils.round((max_mireds - min_mireds) * (math.abs(step_percent_change)/100.0)) + device:send(clusters.ColorControl.server.commands.StepColorTemperature(device, endpoint_id, step_mode, step_size_in_mireds, fields.TRANSITION_TIME_FAST, min_mireds, max_mireds, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF)) +end + + -- [[ VALVE CAPABILITY COMMANDS ]] -- function CapabilityHandlers.handle_valve_open(driver, device, cmd) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index dcc3403780..6eb03b1472 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -1,6 +1,8 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" + local SwitchFields = {} SwitchFields.MOST_RECENT_TEMP = "mostRecentTemp" @@ -13,8 +15,8 @@ SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT = 1000000 -- These values are a "sanity check" to check that values we are getting are reasonable local COLOR_TEMPERATURE_KELVIN_MAX = 15000 local COLOR_TEMPERATURE_KELVIN_MIN = 1000 -SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN -SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX +SwitchFields.COLOR_TEMPERATURE_MIRED_MAX = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) +SwitchFields.COLOR_TEMPERATURE_MIRED_MIN = st_utils.round(SwitchFields.MIRED_KELVIN_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) SwitchFields.SWITCH_LEVEL_LIGHTING_MIN = 1 SwitchFields.CURRENT_HUESAT_ATTR_MIN = 0 @@ -186,10 +188,13 @@ SwitchFields.TEMP_BOUND_RECEIVED = "__temp_bound_received" SwitchFields.TEMP_MIN = "__temp_min" SwitchFields.TEMP_MAX = "__temp_max" -SwitchFields.TRANSITION_TIME = 0 --1/10ths of a second --- When sent with a command, these options mask and override bitmaps cause the command --- to take effect when the switch/light is off. +SwitchFields.TRANSITION_TIME = 0 -- number of 10ths of a second +SwitchFields.TRANSITION_TIME_FAST = 3 -- 0.3 seconds + +-- For Level/Color Control cluster commands, this field indicates which bits in the OptionsOverride field are valid. In this case, we specify that the ExecuteIfOff option (bit 1) may be overridden. SwitchFields.OPTIONS_MASK = 0x01 -SwitchFields.OPTIONS_OVERRIDE = 0x01 +-- the OptionsOverride field's first bit overrides the ExecuteIfOff option, defining whether the command should take effect when the device is off. +SwitchFields.HANDLE_COMMAND_IF_OFF = 0x01 +SwitchFields.IGNORE_COMMAND_IF_OFF = 0x00 return SwitchFields 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 a6d660de4e..abc98591fc 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 @@ -9,7 +9,7 @@ local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("light-color-level-illuminance-motion.yml"), @@ -319,7 +319,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -391,7 +391,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -413,7 +413,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -435,7 +435,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { 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 676a376726..e1af3ad52b 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 @@ -9,7 +9,7 @@ local version = require "version" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device_ep1 = 1 local mock_device_ep2 = 2 @@ -178,7 +178,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { 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 d412c2b35e..db1ae04bfb 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 @@ -9,7 +9,7 @@ local clusters = require "st.matter.generated.zap_clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local button_attr = capabilities.button.button @@ -366,7 +366,7 @@ test.register_coroutine_test( }) test.socket.matter:__expect_send({ mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, mock_device_ep5, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) }) test.socket.matter:__queue_receive({ mock_device.id, 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 86edf45403..6cdf530dbf 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -9,7 +9,7 @@ local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("switch-color-level.yml"), @@ -451,7 +451,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send( { mock_device_no_hue_sat.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColor(mock_device_no_hue_sat, 1, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } ) test.socket.matter:__queue_receive( @@ -513,7 +513,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -595,7 +595,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHueAndSaturation(mock_device, 1, hue, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -669,7 +669,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToHue(mock_device, 1, hue, 0, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -691,7 +691,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToSaturation(mock_device, 1, sat, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, } @@ -713,7 +713,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -985,7 +985,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 165, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -1001,7 +1001,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, 1, 365, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } } } diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua index 3261360910..be01a7bf32 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_lights.lua @@ -10,7 +10,7 @@ test.disable_startup_messages() local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local parent_ep = 10 local child1_ep = 20 @@ -496,7 +496,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -571,7 +571,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua index 92df754303..2370984c97 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_parent_child_plugs.lua @@ -10,7 +10,7 @@ test.disable_startup_messages() local TRANSITION_TIME = 0 local OPTIONS_MASK = 0x01 -local OPTIONS_OVERRIDE = 0x01 +local HANDLE_COMMAND_IF_OFF = 0x01 local parent_ep = 10 local child1_ep = 20 @@ -489,7 +489,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColorTemperature(mock_device, child2_ep, 556, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { @@ -564,7 +564,7 @@ test.register_message_test( direction = "send", message = { mock_device.id, - clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, OPTIONS_OVERRIDE) + clusters.ColorControl.server.commands.MoveToColor(mock_device, child2_ep, 15182, 21547, TRANSITION_TIME, OPTIONS_MASK, HANDLE_COMMAND_IF_OFF) } }, { diff --git a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua new file mode 100644 index 0000000000..a8a8e83b4b --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua @@ -0,0 +1,163 @@ +-- 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 clusters = require "st.matter.clusters" + +local mock_device_color_temp = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-level-colorTemperature.yml"), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + endpoints = { + { + endpoint_id = 1, + clusters = { + {cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"}, + {cluster_id = clusters.ColorControl.ID, cluster_type = "BOTH", feature_map = 30}, + {cluster_id = clusters.LevelControl.ID, cluster_type = "SERVER"} + }, + device_types = { + {device_type_id = 0x0100, device_type_revision = 1}, -- On/Off Light + {device_type_id = 0x010C, device_type_revision = 1} -- Color Temperature Light + } + } + } +}) + +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.LevelControl.attributes.CurrentLevel, + clusters.LevelControl.attributes.MaxLevel, + clusters.LevelControl.attributes.MinLevel, + clusters.ColorControl.attributes.ColorTemperatureMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, + clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, +} + +local function test_init() + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_color_temp) + for i, cluster in ipairs(cluster_subscribe_list) do + if i > 1 then + subscribe_request:merge(cluster:subscribe(mock_device_color_temp)) + end + end + test.socket.matter:__expect_send({mock_device_color_temp.id, subscribe_request}) + test.mock_device.add_test_device(mock_device_color_temp) +end +test.set_test_init_function(test_init) + +local fields = require "switch_utils.fields" + +test.register_message_test( + "Color Temperature Step Command Test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 20 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 187, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { 90 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.DOWN, 840, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessColorTemperatureStep", component = "main", command = "stepColorTemperatureByPercent", args = { -50 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.ColorControl.server.commands.StepColorTemperature(mock_device_color_temp, 1, clusters.ColorControl.types.StepModeEnum.UP, 467, fields.TRANSITION_TIME_FAST, fields.COLOR_TEMPERATURE_MIRED_MIN, fields.COLOR_TEMPERATURE_MIRED_MAX, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + } + } +) + + +test.register_message_test( + "Level Step Command Test", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 25 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 64, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { -50 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.DOWN, 127, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + }, + { + channel = "capability", + direction = "receive", + message = { + mock_device_color_temp.id, + { capability = "statelessSwitchLevelStep", component = "main", command = "stepLevel", args = { 100 } } + } + }, + { + channel = "matter", + direction = "send", + message = { + mock_device_color_temp.id, + clusters.LevelControl.server.commands.Step(mock_device_color_temp, 1, clusters.LevelControl.types.StepModeEnum.UP, 254, fields.TRANSITION_TIME_FAST, fields.OPTIONS_MASK, fields.IGNORE_COMMAND_IF_OFF) + }, + } + } +) + +test.run_registered_tests() From 51f1132278bb930ee34c4b0ee77e167be67c226f Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:29:35 -0600 Subject: [PATCH 426/449] Matter Camera: Include all supported resolutions (#2707) This change adds logic for including the resolutions exposed by the VideoSensorParams and MinViewport attributes in supportedResolutions, rather than only looking at the resolution embedded within RateDistortionTradeOffPoints. --- .../camera_handlers/attribute_handlers.lua | 91 ++++++++----------- .../camera/camera_utils/fields.lua | 2 + .../sub_drivers/camera/camera_utils/utils.lua | 32 +++++++ .../src/test/test_matter_camera.lua | 38 +++++++- 4 files changed, 106 insertions(+), 57 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 484e4c7a15..df99d50f94 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 @@ -113,9 +113,6 @@ end function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver, device, ib, response) if not ib.data.elements then return end local resolutions = {} - local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) - local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) - local emit_capability = max_encoded_pixel_rate ~= nil and max_fps ~= nil for _, v in ipairs(ib.data.elements) do local rate_distortion_trade_off_points = v.elements local width = rate_distortion_trade_off_points.resolution.elements.width.value @@ -124,77 +121,63 @@ function CameraAttributeHandlers.rate_distortion_trade_off_points_handler(driver width = width, height = height }) - if emit_capability then - local fps = camera_utils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) - if fps > 0 then - resolutions[#resolutions].fps = fps - end - end - end - if emit_capability then - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) end device:set_field(camera_fields.SUPPORTED_RESOLUTIONS, resolutions) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end end function CameraAttributeHandlers.max_encoded_pixel_rate_handler(driver, device, ib, response) - local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) - local emit_capability = resolutions ~= nil and max_fps ~= nil - if emit_capability then - for _, v in pairs(resolutions or {}) do - local fps = camera_utils.compute_fps(ib.data.value, v.width, v.height, max_fps) - if fps > 0 then - v.fps = fps - end - end - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) + if max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, ib.data.value, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) end - device:set_field(camera_fields.MAX_ENCODED_PIXEL_RATE, ib.data.value) end function CameraAttributeHandlers.video_sensor_parameters_handler(driver, device, ib, response) if not ib.data.elements then return end - local resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + local sensor_width = ib.data.elements.sensor_width.value + local sensor_height = ib.data.elements.sensor_height.value + local max_fps = ib.data.elements.max_fps.value + device:set_field(camera_fields.MAX_RESOLUTION, { + width = sensor_width, + height = sensor_height + }) + device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ + width = sensor_width, + height = sensor_height, + maxFPS = max_fps + })) local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) - local emit_capability = resolutions ~= nil and max_encoded_pixel_rate ~= nil - local sensor_width, sensor_height, max_fps - for _, v in pairs(ib.data.elements) do - if v.field_id == 0 then - sensor_width = v.value - elseif v.field_id == 1 then - sensor_height = v.value - elseif v.field_id == 2 then - max_fps = v.value - end - end - - if max_fps then - if sensor_width and sensor_height then - device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.videoSensorParameters({ - width = sensor_width, - height = sensor_height, - maxFPS = max_fps - })) - end - if emit_capability then - for _, v in pairs(resolutions or {}) do - local fps = camera_utils.compute_fps(max_encoded_pixel_rate, v.width, v.height, max_fps) - if fps > 0 then - v.fps = fps - end - end - device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(resolutions)) - end - device:set_field(camera_fields.MAX_FRAMES_PER_SECOND, max_fps) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MIN_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) end end function CameraAttributeHandlers.min_viewport_handler(driver, device, ib, response) + if not ib.data.elements then return end device:emit_event_for_endpoint(ib, capabilities.cameraViewportSettings.minViewportResolution({ width = ib.data.elements.width.value, height = ib.data.elements.height.value })) + device:set_field(camera_fields.MIN_RESOLUTION, { + width = ib.data.elements.width.value, + height = ib.data.elements.height.value + }) + local max_encoded_pixel_rate = device:get_field(camera_fields.MAX_ENCODED_PIXEL_RATE) + local max_fps = device:get_field(camera_fields.MAX_FRAMES_PER_SECOND) + if max_encoded_pixel_rate and max_fps and device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) and device:get_field(camera_fields.MAX_RESOLUTION) then + local supported_resolutions = camera_utils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + device:emit_event_for_endpoint(ib, capabilities.videoStreamSettings.supportedResolutions(supported_resolutions)) + end end function CameraAttributeHandlers.allocated_video_streams_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua index 677e2d5dd6..7598b89893 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/fields.lua @@ -10,6 +10,8 @@ CameraFields.MAX_FRAMES_PER_SECOND = "__max_frames_per_second" CameraFields.MAX_VOLUME_LEVEL = "__max_volume_level" CameraFields.MIN_VOLUME_LEVEL = "__min_volume_level" CameraFields.SUPPORTED_RESOLUTIONS = "__supported_resolutions" +CameraFields.MAX_RESOLUTION = "__max_resolution" +CameraFields.MIN_RESOLUTION = "__min_resolution" CameraFields.TRIGGERED_ZONES = "__triggered_zones" CameraFields.VIEWPORT = "__viewport" diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index a93e757c16..1caa9737bb 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -102,6 +102,38 @@ function CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) return math.tointeger(math.floor(fps / fps_step) * fps_step) end +function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, max_fps) + local resolutions = {} + local added_resolutions = {} + + local function add_resolution(width, height) + local key = width .. "x" .. height + if not added_resolutions[key] then + local resolution = { width = width, height = height } + resolution.fps = CameraUtils.compute_fps(max_encoded_pixel_rate, width, height, max_fps) + table.insert(resolutions, resolution) + added_resolutions[key] = true + end + end + + local min_resolution = device:get_field(camera_fields.MIN_RESOLUTION) + if min_resolution then + add_resolution(min_resolution.width, min_resolution.height) + end + + local trade_off_resolutions = device:get_field(camera_fields.SUPPORTED_RESOLUTIONS) + for _, v in pairs(trade_off_resolutions or {}) do + add_resolution(v.width, v.height) + end + + local max_resolution = device:get_field(camera_fields.MAX_RESOLUTION) + if max_resolution then + add_resolution(max_resolution.width, max_resolution.height) + end + + return resolutions +end + function CameraUtils.profile_changed(synced_components, prev_components) if #synced_components ~= #prev_components then return true 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 db8d980fab..930cb069dc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -683,6 +683,18 @@ local function receive_max_encoded_pixel_rate() }) end +local function receive_min_viewport() + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution:build_test_report_data( + mock_device, CAMERA_EP, clusters.CameraAvStreamManagement.types.VideoResolutionStruct({ + width = 1920, + height = 1080 + }) + ) + }) +end + local function receive_video_sensor_params() test.socket.matter:__queue_receive({ mock_device.id, @@ -697,6 +709,15 @@ local function receive_video_sensor_params() }) end +local function emit_min_viewport() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.cameraViewportSettings.minViewportResolution({ + width = 1920, + height = 1080, + })) + ) +end + local function emit_video_sensor_parameters() test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.cameraViewportSettings.videoSensorParameters({ @@ -719,6 +740,11 @@ local function emit_supported_resolutions() width = 3840, height = 2160, fps = 15 + }, + { + width = 7360, + height = 4912, + fps = 0 } })) ) @@ -730,12 +756,14 @@ end -- videoStreamSettings.supportedResolutions is emitted after all three attributes are received. test.register_coroutine_test( - "Rate Distortion Trade Off Points, MaxEncodedPixelRate, VideoSensorParams reports should generate appropriate events", + "Rate Distortion Trade Off Points, MaxEncodedPixelRate, MinViewport, VideoSensorParams reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_rate_distortion_trade_off_points() receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() emit_supported_resolutions() @@ -743,11 +771,13 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Rate Distortion Trade Off Points, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", + "Rate Distortion Trade Off Points, MinViewport, VideoSensorParams, MaxEncodedPixelRate reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_rate_distortion_trade_off_points() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() receive_max_encoded_pixel_rate() @@ -756,11 +786,13 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "MaxEncodedPixelRate, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", + "MaxEncodedPixelRate, MinViewport, VideoSensorParams, Rate Distortion Trade Off Points reports should generate appropriate events", function() update_device_profile() test.wait_for_events() receive_max_encoded_pixel_rate() + receive_min_viewport() + emit_min_viewport() receive_video_sensor_params() emit_video_sensor_parameters() receive_rate_distortion_trade_off_points() From 3d0ff3458fbbc25ad24e92fb301d65f56e4e018e Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:01:53 -0600 Subject: [PATCH 427/449] Matter: Improve component-changed comparisons for modular profile infoChanged events (#2775) --- .../matter-lock/src/new-matter-lock/init.lua | 33 ++++++-- .../air_quality_sensor_utils/utils.lua | 24 ++++-- .../sub_drivers/air_quality_sensor/init.lua | 3 +- .../SmartThings/matter-switch/src/init.lua | 3 +- .../sub_drivers/camera/camera_utils/utils.lua | 18 ----- .../src/sub_drivers/camera/init.lua | 2 +- .../src/switch_utils/device_configuration.lua | 1 - .../matter-switch/src/switch_utils/fields.lua | 2 - .../matter-switch/src/switch_utils/utils.lua | 27 +++++++ .../src/test/test_matter_light_fan.lua | 81 +++++++++++++++++-- 10 files changed, 149 insertions(+), 45 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index 11aa2b0884..a91790cedb 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -22,8 +22,6 @@ 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 MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED" - local RESPONSE_STATUS_MAP = { [DoorLock.types.DlStatus.SUCCESS] = "success", [DoorLock.types.DlStatus.FAILURE] = "failure", @@ -203,7 +201,6 @@ local function match_profile_modular(driver, device) table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) - device:set_field(MODULAR_PROFILE_UPDATED, true) end local function match_profile_switch(driver, device) @@ -241,11 +238,37 @@ local function match_profile_switch(driver, device) device:try_update_metadata({profile = profile_name}) end +local function profile_changed(latest_profile, previous_profile) + if latest_profile.id ~= previous_profile.id then + return true + end + for component_id, synced_component in pairs(latest_profile.components or {}) do + local prev_component = previous_profile.components[component_id] + if prev_component == nil then + return true + end + if #synced_component.capabilities ~= #prev_component.capabilities then + return true + end + -- Build a table of capability IDs from the previous component. Then, use this map to check + -- that all capabilities in the synced component existed in the previous component. + local prev_cap_ids = {} + for _, capability in ipairs(prev_component.capabilities or {}) do + prev_cap_ids[capability.id] = true + end + for _, capability in ipairs(synced_component.capabilities or {}) do + if not prev_cap_ids[capability.id] then + return true + end + end + end + return false +end + local function info_changed(driver, device, event, args) - if device.profile.id == args.old_st_store.profile.id and not device:get_field(MODULAR_PROFILE_UPDATED) then + if not profile_changed(device.profile, args.old_st_store.profile) then return end - device:set_field(MODULAR_PROFILE_UPDATED, nil) for cap_id, attributes in pairs(subscribed_attributes) do if device:supports_capability_by_id(cap_id) then for _, attr in ipairs(attributes) do 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 95ca80964c..5c1726d6d8 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 @@ -77,17 +77,26 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device) end end -function AirQualitySensorUtils.profile_changed(synced_components, prev_components) - if #synced_components ~= #prev_components then +function AirQualitySensorUtils.profile_changed(latest_profile, previous_profile) + if latest_profile.id ~= previous_profile.id then return true end - for _, component in pairs(synced_components or {}) do - if (prev_components[component.id] == nil) or - (#component.capabilities ~= #prev_components[component.id].capabilities) then + for component_id, synced_component in pairs(latest_profile.components or {}) do + local prev_component = previous_profile.components[component_id] + if prev_component == nil then return true end - for _, capability in pairs(component.capabilities or {}) do - if prev_components[component.id][capability.id] == nil then + if #synced_component.capabilities ~= #prev_component.capabilities then + return true + end + -- Build a table of capability IDs from the previous component. Then, use this map to check + -- that all capabilities in the synced component existed in the previous component. + local prev_cap_ids = {} + for _, capability in ipairs(prev_component.capabilities or {}) do + prev_cap_ids[capability.id] = true + end + for _, capability in ipairs(synced_component.capabilities or {}) do + if not prev_cap_ids[capability.id] then return true end end @@ -95,4 +104,5 @@ function AirQualitySensorUtils.profile_changed(synced_components, prev_component return false 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 98b8430c98..41c6514b40 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 @@ -66,8 +66,7 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) end function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id or - aqs_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then + if aqs_utils.profile_changed(device.profile, args.old_st_store.profile) 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-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index cac42e4483..12ee2b3662 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -64,8 +64,7 @@ function SwitchLifecycleHandlers.driver_switched(driver, device) end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then - device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) + if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then if device.network_type == device_lib.NETWORK_TYPE_MATTER then device:subscribe() button_cfg.configure_buttons(device, diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 1caa9737bb..5f3205d73b 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -134,24 +134,6 @@ function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, return resolutions end -function CameraUtils.profile_changed(synced_components, prev_components) - if #synced_components ~= #prev_components then - return true - end - for _, component in pairs(synced_components or {}) do - if (prev_components[component.id] == nil) or - (#component.capabilities ~= #prev_components[component.id].capabilities) then - return true - end - for _, capability in pairs(component.capabilities or {}) do - if prev_components[component.id][capability.id] == nil then - return true - end - end - end - return false -end - function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list) local previous_capability_map = {} local component_sizes = {} diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index f13589ff41..c32e131572 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -47,7 +47,7 @@ function CameraLifecycleHandlers.driver_switched(driver, device) end function CameraLifecycleHandlers.info_changed(driver, device, event, args) - if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then + if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then camera_cfg.initialize_camera_capabilities(device) device:subscribe() if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then 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 8542972320..98a300924f 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -238,7 +238,6 @@ function DeviceConfiguration.match_profile(driver, device) local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) if #fan_device_type_ep_ids > 0 then updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) - device:set_field(fields.MODULAR_PROFILE_UPDATED, true) end -- initialize the main device card with buttons if applicable diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 6eb03b1472..ec620eaa65 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -147,8 +147,6 @@ SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" --- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling. SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" -SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" - SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", BATTERY_SUPPORT = "__battery_support", diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 0592d9a342..16d602cfc1 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -325,6 +325,33 @@ function utils.create_multi_press_values_list(size, supportsHeld) return list end +function utils.profile_changed(latest_profile, previous_profile) + if latest_profile.id ~= previous_profile.id then + return true + end + for component_id, synced_component in pairs(latest_profile.components or {}) do + local prev_component = previous_profile.components[component_id] + if prev_component == nil then + return true + end + if #synced_component.capabilities ~= #prev_component.capabilities then + return true + end + -- Build a table of capability IDs from the previous component. Then, use this map to check + -- that all capabilities in the synced component existed in the previous component. + local prev_cap_ids = {} + for _, capability in ipairs(prev_component.capabilities or {}) do + prev_cap_ids[capability.id] = true + end + for _, capability in ipairs(synced_component.capabilities or {}) do + if not prev_cap_ids[capability.id] then + return true + end + end + end + return false +end + function utils.detect_bridge(device) return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end 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 e1af3ad52b..b5ae89fd7a 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 @@ -16,7 +16,8 @@ local mock_device_ep2 = 2 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Fan Light", - profile = t_utils.get_profile_definition("fan-modular.yml", {}), + profile = t_utils.get_profile_definition("fan-modular.yml", + {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}}), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -58,6 +59,40 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_capabilities_disabled = test.mock_device.build_test_matter_device({ + label = "Matter Fan Light", + profile = t_utils.get_profile_definition("fan-modular.yml", + {enabled_optional_capabilities = {{"main", {}}}}), + manufacturer_info = { + vendor_id = 0x0000, + product_id = 0x0000, + }, + matter_version = { + software = 1, + hardware = 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 = mock_device_ep2, + clusters = { + {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15}, + }, + device_types = { + {device_type_id = 0x002B, device_type_revision = 1,} -- Fan + } + } + } +}) + local CLUSTER_SUBSCRIBE_LIST ={ clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -110,16 +145,48 @@ local function test_init() }) mock_device:expect_metadata_update({ profile = "fan-modular", optional_component_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}} }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - - local updated_device_profile = t_utils.get_profile_definition("fan-modular.yml", - {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}} - ) - 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 test.set_test_init_function(test_init) +test.register_coroutine_test( + "Component-capability update without profile ID update should cause re-subscribe in infoChanged handler", function() + local cluster_subscribe_list ={ + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.PercentCurrent, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end + end + test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed( + {profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={{id="fanSpeedPercent", version=1}, {id="fanMode", version=1}, {id="firmwareUpdate", version=1}, {id="refresh", version=1}}}}}}) + ) + 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.register_coroutine_test( + "No component-capability update an no profile ID update should not cause a re-subscribe in infoChanged handler", function() + local cluster_subscribe_list ={ + clusters.FanControl.attributes.FanModeSequence, + clusters.FanControl.attributes.FanMode, + clusters.FanControl.attributes.PercentCurrent, + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end + end + test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed( + {profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={{id="firmwareUpdate", version=1}, {id="refresh", version=1}}}}}}) + ) + end, + { test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end } +) + + test.register_coroutine_test( "Switch capability should send the appropriate commands", function() test.socket.capability:__queue_receive( From c26bfbed6f8c8ccc716c265f726b0a951652ad0a Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Fri, 13 Feb 2026 14:20:45 -0600 Subject: [PATCH 428/449] Updating zigbee-switch and zwave-switch to use the sub_driver.lua file method --- .../SmartThings/zigbee-switch/src/init.lua | 35 +---------------- .../zigbee-switch/src/sub_drivers.lua | 38 +++++++++++++++++++ drivers/SmartThings/zwave-switch/src/init.lua | 25 +----------- .../zwave-switch/src/sub_drivers.lua | 28 ++++++++++++++ 4 files changed, 68 insertions(+), 58 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-switch/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index a7be3f8801..aa245f5910 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -54,8 +54,6 @@ local device_init = function(driver, device) end end -local lazy_load_if_possible = require "lazy_load_subdriver" - local zigbee_switch_driver_template = { supported_capabilities = { capabilities.switch, @@ -69,38 +67,7 @@ local zigbee_switch_driver_template = { capabilities.relativeHumidityMeasurement, capabilities.temperatureMeasurement, }, - sub_drivers = { - lazy_load_if_possible("non_zigbee_devices"), - lazy_load_if_possible("hanssem"), - lazy_load_if_possible("aqara"), - lazy_load_if_possible("aqara-light"), - lazy_load_if_possible("ezex"), - lazy_load_if_possible("rexense"), - lazy_load_if_possible("sinope"), - lazy_load_if_possible("sinope-dimmer"), - lazy_load_if_possible("zigbee-dimmer-power-energy"), - lazy_load_if_possible("zigbee-metering-plug-power-consumption-report"), - lazy_load_if_possible("jasco"), - lazy_load_if_possible("multi-switch-no-master"), - lazy_load_if_possible("zigbee-dual-metering-switch"), - lazy_load_if_possible("rgb-bulb"), - lazy_load_if_possible("zigbee-dimming-light"), - lazy_load_if_possible("white-color-temp-bulb"), - lazy_load_if_possible("rgbw-bulb"), - (version.api < 16) and lazy_load_if_possible("zll-dimmer-bulb") or nil, - lazy_load_if_possible("ikea-xy-color-bulb"), - lazy_load_if_possible("zll-polling"), - lazy_load_if_possible("zigbee-switch-power"), - lazy_load_if_possible("ge-link-bulb"), - lazy_load_if_possible("bad_on_off_data_type"), - lazy_load_if_possible("robb"), - lazy_load_if_possible("wallhero"), - lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN - lazy_load_if_possible("laisiao"), - lazy_load_if_possible("tuya-multi"), - lazy_load_if_possible("frient"), - lazy_load_if_possible("frient-IO") - }, + sub_drivers = require("sub_drivers"), zigbee_handlers = { global = { [SIMPLE_METERING_ID] = { diff --git a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua new file mode 100644 index 0000000000..c232b40329 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua @@ -0,0 +1,38 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +local version = require "version" + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("non_zigbee_devices"), + lazy_load_if_possible("hanssem"), + lazy_load_if_possible("aqara"), + lazy_load_if_possible("aqara-light"), + lazy_load_if_possible("ezex"), + lazy_load_if_possible("rexense"), + lazy_load_if_possible("sinope"), + lazy_load_if_possible("sinope-dimmer"), + lazy_load_if_possible("zigbee-dimmer-power-energy"), + lazy_load_if_possible("zigbee-metering-plug-power-consumption-report"), + lazy_load_if_possible("jasco"), + lazy_load_if_possible("multi-switch-no-master"), + lazy_load_if_possible("zigbee-dual-metering-switch"), + lazy_load_if_possible("rgb-bulb"), + lazy_load_if_possible("zigbee-dimming-light"), + lazy_load_if_possible("white-color-temp-bulb"), + lazy_load_if_possible("rgbw-bulb"), + (version.api < 16) and lazy_load_if_possible("zll-dimmer-bulb") or nil, + lazy_load_if_possible("ikea-xy-color-bulb"), + lazy_load_if_possible("zll-polling"), + lazy_load_if_possible("zigbee-switch-power"), + lazy_load_if_possible("ge-link-bulb"), + lazy_load_if_possible("bad_on_off_data_type"), + lazy_load_if_possible("robb"), + lazy_load_if_possible("wallhero"), + lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN + lazy_load_if_possible("laisiao"), + lazy_load_if_possible("tuya-multi"), + lazy_load_if_possible("frient"), + lazy_load_if_possible("frient-IO") +} diff --git a/drivers/SmartThings/zwave-switch/src/init.lua b/drivers/SmartThings/zwave-switch/src/init.lua index 405600e962..26cca570a7 100644 --- a/drivers/SmartThings/zwave-switch/src/init.lua +++ b/drivers/SmartThings/zwave-switch/src/init.lua @@ -17,8 +17,6 @@ local SwitchMultilevel = (require "st.zwave.CommandClass.SwitchMultilevel")({ ve local preferencesMap = require "preferences" local configurationsMap = require "configurations" -local lazy_load_if_possible = require "lazy_load_subdriver" - --- Map component to end_points(channels) --- --- @param device st.zwave.Device @@ -120,28 +118,7 @@ local driver_template = { [SwitchMultilevel.STOP_LEVEL_CHANGE] = switch_multilevel_stop_level_change_handler } }, - sub_drivers = { - lazy_load_if_possible("eaton-accessory-dimmer"), - lazy_load_if_possible("inovelli"), - lazy_load_if_possible("dawon-smart-plug"), - lazy_load_if_possible("inovelli-2-channel-smart-plug"), - lazy_load_if_possible("zwave-dual-switch"), - lazy_load_if_possible("eaton-anyplace-switch"), - lazy_load_if_possible("fibaro-wall-plug-us"), - lazy_load_if_possible("dawon-wall-smart-switch"), - lazy_load_if_possible("zooz-power-strip"), - lazy_load_if_possible("aeon-smart-strip"), - lazy_load_if_possible("qubino-switches"), - lazy_load_if_possible("fibaro-double-switch"), - lazy_load_if_possible("fibaro-single-switch"), - lazy_load_if_possible("eaton-5-scene-keypad"), - lazy_load_if_possible("ecolink-switch"), - lazy_load_if_possible("multi-metering-switch"), - lazy_load_if_possible("zooz-zen-30-dimmer-relay"), - lazy_load_if_possible("multichannel-device"), - lazy_load_if_possible("aeotec-smart-switch"), - lazy_load_if_possible("aeotec-heavy-duty") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, infoChanged = info_changed, diff --git a/drivers/SmartThings/zwave-switch/src/sub_drivers.lua b/drivers/SmartThings/zwave-switch/src/sub_drivers.lua new file mode 100644 index 0000000000..b4cd3d57e9 --- /dev/null +++ b/drivers/SmartThings/zwave-switch/src/sub_drivers.lua @@ -0,0 +1,28 @@ + +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" + +return { + lazy_load_if_possible("eaton-accessory-dimmer"), + lazy_load_if_possible("inovelli"), + lazy_load_if_possible("dawon-smart-plug"), + lazy_load_if_possible("inovelli-2-channel-smart-plug"), + lazy_load_if_possible("zwave-dual-switch"), + lazy_load_if_possible("eaton-anyplace-switch"), + lazy_load_if_possible("fibaro-wall-plug-us"), + lazy_load_if_possible("dawon-wall-smart-switch"), + lazy_load_if_possible("zooz-power-strip"), + lazy_load_if_possible("aeon-smart-strip"), + lazy_load_if_possible("qubino-switches"), + lazy_load_if_possible("fibaro-double-switch"), + lazy_load_if_possible("fibaro-single-switch"), + lazy_load_if_possible("eaton-5-scene-keypad"), + lazy_load_if_possible("ecolink-switch"), + lazy_load_if_possible("multi-metering-switch"), + lazy_load_if_possible("zooz-zen-30-dimmer-relay"), + lazy_load_if_possible("multichannel-device"), + lazy_load_if_possible("aeotec-smart-switch"), + lazy_load_if_possible("aeotec-heavy-duty") +} From d18914282669e4365164adf6699dfa5cdf818590 Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Wed, 18 Feb 2026 08:35:01 -0600 Subject: [PATCH 429/449] Revert "CHAD-17070: zigbee-lock lazy loading of subdrivers" This reverts commit aa71a9a9cd3b17cf00699b3dec8d24db6052ad83. --- .../zigbee-lock/src/configurations.lua | 15 ++++++-- drivers/SmartThings/zigbee-lock/src/init.lua | 23 ++++++++++--- .../zigbee-lock/src/lazy_load_subdriver.lua | 15 -------- .../src/lock-without-codes/can_handle.lua | 14 -------- .../src/lock-without-codes/fingerprints.lua | 9 ----- .../src/lock-without-codes/init.lua | 30 +++++++++++++--- .../zigbee-lock/src/lock_utils.lua | 15 ++++++-- .../zigbee-lock/src/samsungsds/can_handle.lua | 11 ------ .../zigbee-lock/src/samsungsds/init.lua | 20 ++++++++--- .../zigbee-lock/src/sub_drivers.lua | 11 ------ .../zigbee-lock/src/test/test_c2o_lock.lua | 15 ++++++-- .../src/test/test_generic_lock_migration.lua | 17 ++++++++-- ..._yale_fingerprint_bad_battery_reporter.lua | 15 ++++++-- .../zigbee-lock/src/test/test_zigbee_lock.lua | 15 ++++++-- .../test/test_zigbee_lock_code_migration.lua | 15 ++++++-- .../test_zigbee_yale-bad-battery-reporter.lua | 15 ++++++-- .../test_zigbee_yale-fingerprint-lock.lua | 15 ++++++-- .../zigbee-lock/src/test/test_zigbee_yale.lua | 15 ++++++-- .../src/yale-fingerprint-lock/can_handle.lua | 14 -------- .../yale-fingerprint-lock/fingerprints.lua | 11 ------ .../src/yale-fingerprint-lock/init.lua | 32 ++++++++++++++--- .../zigbee-lock/src/yale/can_handle.lua | 11 ------ .../SmartThings/zigbee-lock/src/yale/init.lua | 23 ++++++++++--- .../zigbee-lock/src/yale/sub_drivers.lua | 8 ----- .../yale-bad-battery-reporter/can_handle.lua | 14 -------- .../fingerprints.lua | 13 ------- .../yale/yale-bad-battery-reporter/init.lua | 34 ++++++++++++++++--- 27 files changed, 268 insertions(+), 177 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/sub_drivers.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua 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 delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/configurations.lua b/drivers/SmartThings/zigbee-lock/src/configurations.lua index 88e4e59f80..a2429252b0 100644 --- a/drivers/SmartThings/zigbee-lock/src/configurations.lua +++ b/drivers/SmartThings/zigbee-lock/src/configurations.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 94f5adc0c4..ce6894b868 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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. -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" @@ -435,7 +445,12 @@ local zigbee_lock_driver = { [capabilities.refresh.commands.refresh.NAME] = refresh } }, - sub_drivers = require("sub_drivers"), + sub_drivers = { + require("samsungsds"), + require("yale"), + require("yale-fingerprint-lock"), + require("lock-without-codes") + }, lifecycle_handlers = { doConfigure = do_configure, added = device_added, diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua deleted file mode 100644 index 0bee6d2a75..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(sub_driver_name) - -- gets the current lua libs api version - local version = require "version" - local ZigbeeDriver = require "st.zigbee" - if version.api >= 16 then - return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) - elseif version.api >= 9 then - return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end -end 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-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua deleted file mode 100644 index 63ae82b46c..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} - -return LOCK_WITHOUT_CODES_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index e5c6de3408..7272991459 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 configurationMap = require "configurations" local clusters = require "st.zigbee.zcl.clusters" @@ -9,7 +19,19 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} +local function can_handle_lock_without_codes(opts, driver, device) + for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_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_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -73,7 +95,7 @@ local lock_without_codes = { } } }, - can_handle = require("lock-without-codes.can_handle"), + can_handle = can_handle_lock_without_codes } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index a02a59963c..0a36a9685e 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 utils = require "st.utils" local capabilities = require "st.capabilities" local json = require "st.json" diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua deleted file mode 100644 index c483b2fe27..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function samsungsds_can_handle(opts, driver, device, ...) - if device:get_manufacturer() == "SAMSUNG SDS" then - return true, require("samsungsds") - end - return false -end - -return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index fff290df5d..b529dd3fd1 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -102,7 +112,9 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = require("samsungsds.can_handle"), + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "SAMSUNG SDS" + end } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua deleted file mode 100644 index ff4bf8980d..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua +++ /dev/null @@ -1,11 +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("samsungsds"), - lazy_load_if_possible("yale"), - 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_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index 146c628b8b..b6fa3d1323 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua index f287300f60..ed4ce6e3cc 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua @@ -1,5 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2023 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" @@ -34,4 +45,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua index 4f50c3c24a..d499d7ff66 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 3ed037cd54..80d10d092e 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" 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 index 7950e3f62d..1aa9432933 100644 --- 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 @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" 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 index b8f4c386d9..ee1745e3b7 100644 --- 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 @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" 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 7cda71cdb3..2255c063a3 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 @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 75ad49a1f5..34b6881028 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" 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 deleted file mode 100644 index a80632bf80..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local yale_fingerprint_lock_models = 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 - return true, require("yale-fingerprint-lock") - end - end - return false -end - -return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua deleted file mode 100644 index b3db27d719..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -return YALE_FINGERPRINT_LOCK 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..9d0a0b4148 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -9,7 +19,21 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} +local yale_fingerprint_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) @@ -24,7 +48,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = require("yale-fingerprint-lock.can_handle"), + can_handle = yale_fingerprint_lock_models } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua deleted file mode 100644 index 54340c7811..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -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") - end - return false -end - -return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 8ba98b2aa8..73e984036e 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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. -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" @@ -142,7 +151,11 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - sub_drivers = require("yale.sub_drivers"), + + sub_drivers = { require("yale.yale-bad-battery-reporter") }, + can_handle = function(opts, driver, device, ...) + return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" + end } return yale_door_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 diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua deleted file mode 100644 index cbb7c3404f..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} - -return BAD_YALE_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua index 3b77f32563..59fdbf228b 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua @@ -1,11 +1,37 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} +local is_bad_yale_lock_models = function(opts, driver, device) + for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) @@ -20,7 +46,7 @@ local bad_yale_driver = { } } }, - can_handle = require("yale.yale-bad-battery-reporter.can_handle"), + can_handle = is_bad_yale_lock_models } return bad_yale_driver From ff4ea0d58ac1b76a8a637b858d7b013c161081d9 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:19 -0600 Subject: [PATCH 430/449] CHAD-17070: zigbee-lock lazy loading of subdrivers --- .../zigbee-lock/src/configurations.lua | 15 ++------ drivers/SmartThings/zigbee-lock/src/init.lua | 23 +++---------- .../zigbee-lock/src/lazy_load_subdriver.lua | 15 ++++++++ .../src/lock-without-codes/can_handle.lua | 14 ++++++++ .../src/lock-without-codes/fingerprints.lua | 9 +++++ .../src/lock-without-codes/init.lua | 30 +++------------- .../zigbee-lock/src/lock_utils.lua | 15 ++------ .../zigbee-lock/src/samsungsds/can_handle.lua | 11 ++++++ .../zigbee-lock/src/samsungsds/init.lua | 20 +++-------- .../zigbee-lock/src/sub_drivers.lua | 11 ++++++ .../zigbee-lock/src/test/test_c2o_lock.lua | 15 ++------ .../src/test/test_generic_lock_migration.lua | 17 ++-------- ..._yale_fingerprint_bad_battery_reporter.lua | 15 ++------ .../zigbee-lock/src/test/test_zigbee_lock.lua | 15 ++------ .../test/test_zigbee_lock_code_migration.lua | 15 ++------ .../test_zigbee_yale-bad-battery-reporter.lua | 15 ++------ .../test_zigbee_yale-fingerprint-lock.lua | 15 ++------ .../zigbee-lock/src/test/test_zigbee_yale.lua | 15 ++------ .../src/yale-fingerprint-lock/can_handle.lua | 14 ++++++++ .../yale-fingerprint-lock/fingerprints.lua | 11 ++++++ .../src/yale-fingerprint-lock/init.lua | 32 +++-------------- .../zigbee-lock/src/yale/can_handle.lua | 11 ++++++ .../SmartThings/zigbee-lock/src/yale/init.lua | 23 +++---------- .../zigbee-lock/src/yale/sub_drivers.lua | 8 +++++ .../yale-bad-battery-reporter/can_handle.lua | 14 ++++++++ .../fingerprints.lua | 13 +++++++ .../yale/yale-bad-battery-reporter/init.lua | 34 +++---------------- 27 files changed, 177 insertions(+), 268 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/configurations.lua b/drivers/SmartThings/zigbee-lock/src/configurations.lua index a2429252b0..88e4e59f80 100644 --- a/drivers/SmartThings/zigbee-lock/src/configurations.lua +++ b/drivers/SmartThings/zigbee-lock/src/configurations.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index ce6894b868..94f5adc0c4 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Zigbee Driver utilities local defaults = require "st.zigbee.defaults" @@ -445,12 +435,7 @@ local zigbee_lock_driver = { [capabilities.refresh.commands.refresh.NAME] = refresh } }, - sub_drivers = { - require("samsungsds"), - require("yale"), - require("yale-fingerprint-lock"), - require("lock-without-codes") - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { doConfigure = do_configure, added = device_added, diff --git a/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end 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 new file mode 100644 index 0000000000..543e43a8b1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua @@ -0,0 +1,14 @@ +-- 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-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua new file mode 100644 index 0000000000..63ae82b46c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local LOCK_WITHOUT_CODES_FINGERPRINTS = { + { model = "E261-KR0B0Z0-HA" }, + { mfr = "Danalock", model = "V3-BTZB" } +} + +return LOCK_WITHOUT_CODES_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua index 7272991459..e5c6de3408 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local configurationMap = require "configurations" local clusters = require "st.zigbee.zcl.clusters" @@ -19,19 +9,7 @@ local capabilities = require "st.capabilities" local DoorLock = clusters.DoorLock local PowerConfiguration = clusters.PowerConfiguration -local LOCK_WITHOUT_CODES_FINGERPRINTS = { - { model = "E261-KR0B0Z0-HA" }, - { mfr = "Danalock", model = "V3-BTZB" } -} -local function can_handle_lock_without_codes(opts, driver, device) - for _, fingerprint in ipairs(LOCK_WITHOUT_CODES_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_init(driver, device) local configuration = configurationMap.get_device_configuration(device) @@ -95,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = can_handle_lock_without_codes + can_handle = require("lock-without-codes.can_handle"), } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua index 0a36a9685e..a02a59963c 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- 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" diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua new file mode 100644 index 0000000000..c483b2fe27 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function samsungsds_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "SAMSUNG SDS" then + return true, require("samsungsds") + end + return false +end + +return samsungsds_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index b529dd3fd1..fff290df5d 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local device_management = require "st.zigbee.device_management" local clusters = require "st.zigbee.zcl.clusters" @@ -112,9 +102,7 @@ local samsung_sds_driver = { added = device_added, init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "SAMSUNG SDS" - end + can_handle = require("samsungsds.can_handle"), } return samsung_sds_driver diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..ff4bf8980d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/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("samsungsds"), + lazy_load_if_possible("yale"), + 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_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index b6fa3d1323..146c628b8b 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua index ed4ce6e3cc..f287300f60 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua @@ -1,16 +1,5 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" @@ -45,4 +34,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua index d499d7ff66..4f50c3c24a 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua index 80d10d092e..3ed037cd54 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 index 1aa9432933..7950e3f62d 100644 --- 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 index ee1745e3b7..b8f4c386d9 100644 --- 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 2255c063a3..7cda71cdb3 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 @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua index 34b6881028..75ad49a1f5 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test" 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 new file mode 100644 index 0000000000..a80632bf80 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local yale_fingerprint_lock_models = 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 + return true, require("yale-fingerprint-lock") + end + end + return false +end + +return yale_fingerprint_lock_models diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua new file mode 100644 index 0000000000..b3db27d719 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YALE_FINGERPRINT_LOCK = { + { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return YALE_FINGERPRINT_LOCK 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 9d0a0b4148..b78d043784 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -19,21 +9,7 @@ local LockCodes = capabilities.lockCodes local YALE_FINGERPRINT_MAX_CODES = 0x1E -local YALE_FINGERPRINT_LOCK = { - { mfr = "ASSA ABLOY iRevo", model = "iZBModule01" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "0700000001" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local yale_fingerprint_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(YALE_FINGERPRINT_LOCK) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local handle_max_codes = function(driver, device, value) device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) @@ -48,7 +24,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = yale_fingerprint_lock_models + can_handle = require("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/yale/can_handle.lua new file mode 100644 index 0000000000..54340c7811 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +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") + end + return false +end + +return yale_can_handle diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 73e984036e..8ba98b2aa8 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + -- Zigbee Spec Utils local clusters = require "st.zigbee.zcl.clusters" @@ -151,11 +142,7 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - - sub_drivers = { require("yale.yale-bad-battery-reporter") }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" - end + sub_drivers = require("yale.sub_drivers"), } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua new file mode 100644 index 0000000000..4b546979d3 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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 new file mode 100644 index 0000000000..67169e9268 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua @@ -0,0 +1,14 @@ +-- 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 diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua new file mode 100644 index 0000000000..cbb7c3404f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local BAD_YALE_LOCK_FINGERPRINTS = { + { mfr = "Yale", model = "YRD220/240 TSDB" }, + { mfr = "Yale", model = "YRL220 TS LL" }, + { mfr = "Yale", model = "YRD210 PB DB" }, + { mfr = "Yale", model = "YRL210 PB LL" }, + { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, + { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } +} + +return BAD_YALE_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua index 59fdbf228b..3b77f32563 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua @@ -1,37 +1,11 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" -local BAD_YALE_LOCK_FINGERPRINTS = { - { mfr = "Yale", model = "YRD220/240 TSDB" }, - { mfr = "Yale", model = "YRL220 TS LL" }, - { mfr = "Yale", model = "YRD210 PB DB" }, - { mfr = "Yale", model = "YRL210 PB LL" }, - { mfr = "ASSA ABLOY iRevo", model = "c700000202" }, - { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } -} -local is_bad_yale_lock_models = function(opts, driver, device) - for _, fingerprint in ipairs(BAD_YALE_LOCK_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) @@ -46,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = is_bad_yale_lock_models + can_handle = require("yale.yale-bad-battery-reporter.can_handle"), } return bad_yale_driver From ef1fcda6853817bd73db139dee4dc0a2fd73cb72 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 18 Feb 2026 15:18:51 -0600 Subject: [PATCH 431/449] hotfix/CHAD-17070: Added test and reverting of dropped `can_handle` in yale lock --- .../src/test/test_zigbee_lock_v10.lua | 778 ++++++++++++++++++ .../SmartThings/zigbee-lock/src/yale/init.lua | 1 + 2 files changed, 779 insertions(+) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua 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 new file mode 100644 index 0000000000..c4e28dcd0d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua @@ -0,0 +1,778 @@ +-- 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 capabilities = require "st.capabilities" +-- Note: This is not the proper way to test against previous versions. +-- Instead, testing should be run against different lua lib artifacts +local version = require "version" +version.api = 10 + +local DoorLockState = DoorLock.attributes.LockState +local OperationEventCode = DoorLock.types.OperationEventCode +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode + +local json = require "dkjson" + +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) + +local expect_reload_all_codes_messages = function() + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, + true) }) + 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) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 0) }) +end + +test.register_coroutine_test( + "Configure should configure all necessary attributes and begin reading codes", + function() + 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" }) + test.wait_for_events() + + test.mock_time.advance_time(2) + expect_reload_all_codes_messages() + + end +) + +test.register_coroutine_test( + "Refresh should read expected 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, 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)}) + end +) + +test.register_message_test( + "Lock status reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, + DoorLockState.LOCKED) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + } + } +) + +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(28)) + } + } +) + +test.register_message_test( + "Lock operation event reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x02, + OperationEventCode.LOCK, + 0x0000, + "", + 0x0000, + "") } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({ data = { method = "manual" } })) + } + } +) + +test.register_message_test( + "Pin response reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x02, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 set", + { data = { codeName = "Code 2" }, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["2"] = "Code 2"} ), { visibility = { displayed = false } })) + } + } +) + +test.register_message_test( + "Sending the lock command should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "lock", component = "main", command = "lock", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.LockDoor(mock_device) } + } + } +) + +test.register_message_test( + "Min lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(4, { visibility = { displayed = false }})) + } + } +) + +test.register_message_test( + "Max lock code length report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 4) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(4, { visibility = { displayed = false }})) + } + } +) + +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(16, { visibility = { displayed = false }})) + } + } +) + +test.register_coroutine_test( + "Reloading all codes of an unconfigured lock should generate correct attribute checks", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {} } }) + expect_reload_all_codes_messages() + end +) + +test.register_message_test( + "Requesting a user code should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = capabilities.lockCodes.ID, command = "requestCode", args = { 1 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) } + } + } +) + +test.register_coroutine_test( + "Deleting a user code should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 set", + { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode( {["1"] = "Code 1"} ), { visibility = { displayed = false }}) + )) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "deleteCode", 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.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.socket.zigbee:__queue_receive({ mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + "")}) + 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.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + end +) + +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, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + } + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + 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.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) + end +) + +local function init_code_slot(slot_number, name, device) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { slot_number, "1234", name } } }) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.SetPINCode(device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zigbee:__expect_send( + { + device.id, + DoorLock.server.commands.GetPINCode(device, slot_number) + } + ) + test.wait_for_events() + test.socket.zigbee:__queue_receive( + { + device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + device, + slot_number, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(device:generate_test_message("main", + capabilities.lockCodes.codeChanged(slot_number .. " set", { data = { codeName = name }, state_change = true })) + ) +end + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "nameSlot", args = { 1, "foo" } } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Setting a user code name via setCode should be handled", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false } }))) + test.wait_for_events() + + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "", "foo"} } }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("1 renamed", {state_change = true}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "Calling updateCodes should send properly spaced commands", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 2, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "2345" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "3456" + ) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 1) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 2) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 3) + }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 4) + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Setting all user codes should result in a code set event for each", + function () + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "updateCodes", args = {{code1 = "1234", code2 = "2345", code3 = "3456", code4 = ""}}}}) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "2345")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 2) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.server.commands.SetPINCode(mock_device, 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "3456")}) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 3) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 4) }) + test.mock_time.advance_time(2) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 4) }) + test.wait_for_events() + end +) + +test.register_message_test( + "Master code programming event should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x00, + ProgrammingEventCode.MASTER_CODE_CHANGED, + 0, + "1234", + DoorLockUserType.MASTER_USER, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + } + } +) + +test.register_message_test( + "The lock reporting a single code has been set should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_ADDED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "Code 1"}, state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } })) + } + } +) + +test.register_coroutine_test( + "The lock reporting a code has been deleted should be handled", + 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.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 1, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + ) + 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.lockCodes(json.encode({}), { visibility = { displayed = false } }))) + end +) + +test.register_coroutine_test( + "The lock reporting that all codes have been deleted should be handled", + 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 } }))) + 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 } }))) + 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.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x0, + ProgrammingEventCode.PIN_CODE_DELETED, + 0xFF, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.AVAILABLE, + 0x0000, + "data" + ) + } + ) + + test.socket.capability:__set_channel_ordering("relaxed") + 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({}), { visibility = { displayed = false } }))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "The lock reporting unlock via code should include the code info in the report", + 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.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + end +) + +test.register_coroutine_test( + "Lock state attribute reports (after the first) should be delayed if they come before event notifications ", + 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.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.UNLOCKED)}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked() + ) + ) + test.mock_time.advance_time(2) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.UNLOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2) + test.timer.__create_and_queue_test_time_advance_timer(2.5, "oneshot") + test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.LockState:build_test_attr_report(mock_device, DoorLockState.LOCKED)}) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + mock_device, + 0x00, -- 0 = keypad + OperationEventCode.LOCK, + 0x0001, + "1234", + 0x0000, + "" + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "keypad", codeId = "1", codeName = "Code 1" } }) + ) + ) + test.mock_time.advance_time(2.5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked() + ) + ) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/yale/init.lua index 8ba98b2aa8..c315fbfa06 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale/init.lua @@ -143,6 +143,7 @@ local yale_door_lock_driver = { } }, sub_drivers = require("yale.sub_drivers"), + can_handle = require("yale.can_handle"), } return yale_door_lock_driver From 2c1799f31c66028286bf6e192a6308154b3acbc5 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Fri, 13 Feb 2026 16:40:38 -0600 Subject: [PATCH 432/449] update ikea scroll profile category to Button --- drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index cce4b68d2d..e07006ce9d 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -14,7 +14,7 @@ components: - id: refresh version: 1 categories: - - name: RemoteController + - name: Button - id: group2 label: Group 2 capabilities: @@ -23,7 +23,7 @@ components: - id: knob version: 1 categories: - - name: RemoteController + - name: Button - id: group3 label: Group 3 capabilities: @@ -32,4 +32,4 @@ components: - id: knob version: 1 categories: - - name: RemoteController + - name: Button From 024c47140f6a857886597a509efdc47dab4650ef Mon Sep 17 00:00:00 2001 From: Steven Green Date: Thu, 19 Feb 2026 13:59:55 -0800 Subject: [PATCH 433/449] WWSTCERT-10417 ubisys dimmer D1-R --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 1c26d5d6c8..6bb32c5978 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -615,6 +615,11 @@ zigbeeManufacturer: manufacturer: ubisys model: "1151" deviceProfileName: switch-power + - id: "ubisys/D1-R (5603)" + deviceLabel: Dimmer D1-R + manufacturer: ubisys + model: "D1-R (5603)" + deviceProfileName: switch-level-power - id: "Megaman/AD-DimmableLight3001" deviceLabel: INGENIUM Light manufacturer: Megaman From 58de43e1941183e03db917df39c6dbf78107d208 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:09 -0600 Subject: [PATCH 434/449] CHAD-17079: zigbee-valve lazy load sub-drivers --- .../zigbee-valve/src/ezex/can_handle.lua | 12 +++++++++++ .../zigbee-valve/src/ezex/init.lua | 20 ++++-------------- drivers/SmartThings/zigbee-valve/src/init.lua | 21 ++++--------------- .../zigbee-valve/src/lazy_load_subdriver.lua | 15 +++++++++++++ .../zigbee-valve/src/sinope/can_handle.lua | 11 ++++++++++ .../zigbee-valve/src/sinope/init.lua | 20 ++++-------------- .../zigbee-valve/src/sub_drivers.lua | 9 ++++++++ .../zigbee-valve/src/test/test_ezex_valve.lua | 16 +++----------- .../src/test/test_sinope_valve.lua | 16 +++----------- .../src/test/test_zigbee_valve.lua | 16 +++----------- 10 files changed, 68 insertions(+), 88 deletions(-) create mode 100644 drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-valve/src/sub_drivers.lua diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua new file mode 100644 index 0000000000..f16f04858c --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/ezex/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function ezex_can_handle(opts, driver, device, ...) + local clusters = require "st.zigbee.zcl.clusters" + if device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) then + return true, require("ezex") + end + return false +end + +return ezex_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua index 47ced66806..c32fe60bac 100644 --- a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -71,9 +61,7 @@ local ezex_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "E253-KR0B0ZX-HA" and not device:supports_server_cluster(clusters.PowerConfiguration.ID) - end + can_handle = require("ezex.can_handle"), } return ezex_valve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 6d355de8ec..1840b55be0 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -51,10 +41,7 @@ local zigbee_valve_driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("sinope"), - require("ezex") - }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua new file mode 100644 index 0000000000..f533d120e0 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sinope/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function sinope_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "Sinope Technologies" then + return true, require("sinope") + end + return false +end + +return sinope_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua index 6a0075cd33..ab3786511c 100644 --- a/drivers/SmartThings/zigbee-valve/src/sinope/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/sinope/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local battery_defaults = require "st.zigbee.defaults.battery_defaults" @@ -59,9 +49,7 @@ local sinope_valve = { lifecycle_handlers = { init = device_init }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "Sinope Technologies" - end + can_handle = require("sinope.can_handle"), } return sinope_valve diff --git a/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua new file mode 100644 index 0000000000..258579c7fb --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua @@ -0,0 +1,9 @@ +-- 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("sinope"), + lazy_load_if_possible("ezex"), +} +return sub_drivers 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 eb96363d99..e925491c87 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 344c6b0814..8710cef0c9 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local t_utils = require "integration_test.utils" 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 baa4252bd2..5b4756f207 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" From fafa569efc940832ddd63540c8f6d0e48f9e62a5 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:16 -0600 Subject: [PATCH 435/449] CHAD-17089: zwave-lock lazy loading of subdrivers --- .../src/apiv6_bugfix/can_handle.lua | 16 +++++++++++++ .../zwave-lock/src/apiv6_bugfix/init.lua | 13 ++++------- drivers/SmartThings/zwave-lock/src/init.lua | 23 +++---------------- .../zwave-lock/src/keywe-lock/can_handle.lua | 12 ++++++++++ .../zwave-lock/src/keywe-lock/init.lua | 23 ++++--------------- .../zwave-lock/src/lazy_load_subdriver.lua | 18 +++++++++++++++ .../src/samsung-lock/can_handle.lua | 12 ++++++++++ .../zwave-lock/src/samsung-lock/init.lua | 23 ++++--------------- .../src/schlage-lock/can_handle.lua | 12 ++++++++++ .../zwave-lock/src/schlage-lock/init.lua | 23 ++++--------------- .../zwave-lock/src/sub_drivers.lua | 12 ++++++++++ .../zwave-lock/src/test/test_keywe_lock.lua | 16 +++---------- .../zwave-lock/src/test/test_lock_battery.lua | 16 +++---------- .../zwave-lock/src/test/test_samsung_lock.lua | 16 +++---------- .../zwave-lock/src/test/test_schlage_lock.lua | 16 +++---------- .../zwave-lock/src/test/test_zwave_lock.lua | 16 +++---------- .../test/test_zwave_lock_code_migration.lua | 16 +++---------- .../src/zwave-alarm-v1-lock/can_handle.lua | 11 +++++++++ .../src/zwave-alarm-v1-lock/init.lua | 21 ++++------------- 19 files changed, 134 insertions(+), 181 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..8a9b8cc6cc --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua index 0204b7b2d5..94dc5975ab 100644 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua @@ -1,14 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -20,7 +15,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index b83b196256..925452c431 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -182,13 +171,7 @@ local driver_template = { [Time.GET] = time_get_handler -- used by DanaLock } }, - sub_drivers = { - require("zwave-alarm-v1-lock"), - require("schlage-lock"), - require("samsung-lock"), - require("keywe-lock"), - require("apiv6_bugfix"), - } + sub_drivers = require("sub_drivers"), } defaults.register_for_default_handlers(driver_template, driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..d8bcd5756e --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/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("keywe-lock") + end + return false +end + +return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index d39aa45d1c..a51af26e00 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -23,13 +13,8 @@ local LockDefaults = require "st.zwave.defaults.lock" local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local TamperDefaults = require "st.zwave.defaults.tamperAlert" -local KEYWE_MFR = 0x037B local TAMPER_CLEAR_DELAY = 10 -local function can_handle_keywe_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == KEYWE_MFR -end - 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 @@ -80,7 +65,7 @@ local keywe_lock = { doConfigure = do_configure }, NAME = "Keywe Lock", - can_handle = can_handle_keywe_lock, + can_handle = require("keywe-lock.can_handle"), } return keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..e9222cb8fb --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 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") + 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 813c6217b4..b2f4f60975 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -28,11 +18,6 @@ local get_lock_codes = LockCodesDefaults.get_lock_codes local clear_code_state = LockCodesDefaults.clear_code_state local code_deleted = LockCodesDefaults.code_deleted -local SAMSUNG_MFR = 0x022E - -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SAMSUNG_MFR -end local function get_ongoing_code_set(device) local code_id @@ -105,7 +90,7 @@ local samsung_lock = { doConfigure = do_configure }, NAME = "Samsung Lock", - can_handle = can_handle_samsung_lock, + can_handle = require("samsung-lock.can_handle"), } return 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 new file mode 100644 index 0000000000..e9f3cfb84c --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 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") + 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 67e649d869..6b22049beb 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -27,15 +17,10 @@ local Association = (require "st.zwave.CommandClass.Association")({version=1}) local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local SCHLAGE_MFR = 0x003B local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - return device.zwave_manufacturer_id == SCHLAGE_MFR -end - local function set_code_length(self, device, cmd) local length = cmd.args.length if length >= 4 and length <= 8 then @@ -187,7 +172,7 @@ local schlage_lock = { doConfigure = do_configure, }, NAME = "Schlage Lock", - can_handle = can_handle_schlage_lock, + can_handle = require("schlage-lock.can_handle"), } return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua new file mode 100644 index 0000000000..46700ce154 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -0,0 +1,12 @@ +-- 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("zwave-alarm-v1-lock"), + lazy_load_if_possible("schlage-lock"), + lazy_load_if_possible("samsung-lock"), + lazy_load_if_possible("keywe-lock"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers 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 d8e8ecc1bc..0266391d85 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 9aac02c6b2..7667842e61 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 7707b8a850..9dc1e38bcf 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 b1a5964502..189184f19e 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua index 52144295b3..b105707c7d 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 index e4a9b50758..a6b0b5a2a3 100644 --- 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 new file mode 100644 index 0000000000..7bb54f23f2 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 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") + 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 44d978999b..d7c862f22a 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,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -35,9 +25,6 @@ local METHOD = { --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 -end --- Default handler for alarm command class reports, these were largely OEM-defined --- @@ -159,7 +146,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-alarm-v1-lock.can_handle"), } return zwave_lock From 91b1c7139465424525288612f50a720c4aaf98a6 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:37 -0600 Subject: [PATCH 436/449] CHAD-17094: zwave-smoke-alarm lazy loading of sub-drivers --- .../src/apiv6_bugfix/can_handle.lua | 16 +++++++++ .../src/apiv6_bugfix/init.lua | 13 +++---- .../src/fibaro-smoke-sensor/can_handle.lua | 14 ++++++++ .../src/fibaro-smoke-sensor/fingerprints.lua | 11 ++++++ .../src/fibaro-smoke-sensor/init.lua | 32 +++-------------- .../zwave-smoke-alarm/src/init.lua | 23 +++--------- .../src/lazy_load_subdriver.lua | 18 ++++++++++ .../zwave-smoke-alarm/src/preferences.lua | 16 ++------- .../zwave-smoke-alarm/src/sub_drivers.lua | 11 ++++++ .../src/test/test_fibaro_co_sensor_zw5.lua | 16 ++------- .../src/test/test_fibaro_smoke_sensor.lua | 16 ++------- .../src/test/test_zwave_alarm_v1.lua | 16 ++------- .../src/test/test_zwave_co_detector.lua | 16 ++------- .../src/test/test_zwave_smoke_detector.lua | 16 ++------- .../zwave-smoke-co-alarm-v1/can_handle.lua | 13 +++++++ .../src/zwave-smoke-co-alarm-v1/init.lua | 28 ++------------- .../zwave-smoke-co-alarm-v2/can_handle.lua | 14 ++++++++ .../fibaro-co-sensor-zw5/can_handle.lua | 14 ++++++++ .../fibaro-co-sensor-zw5/fingerprints.lua | 9 +++++ .../fibaro-co-sensor-zw5/init.lua | 30 +++------------- .../zwave-smoke-co-alarm-v2/fingerprints.lua | 9 +++++ .../src/zwave-smoke-co-alarm-v2/init.lua | 35 ++++--------------- .../zwave-smoke-co-alarm-v2/sub_drivers.lua | 8 +++++ 23 files changed, 180 insertions(+), 214 deletions(-) create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua create mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua new file mode 100644 index 0000000000..8a9b8cc6cc --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle(opts, driver, device, cmd, ...) + local cc = require "st.zwave.CommandClass" + local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + return true, require("apiv6_bugfix") + end + return false +end + +return can_handle diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua index 0204b7b2d5..94dc5975ab 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua @@ -1,14 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - return version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION -end - local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -20,7 +15,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = can_handle + can_handle = require("apiv6_bugfix.can_handle"), } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua new file mode 100644 index 0000000000..4a3f51183b --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("fibaro-smoke-sensor.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("fibaro-smoke-sensor") + end + end + return false +end + +return can_handle_fibaro_smoke_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua new file mode 100644 index 0000000000..81c04eccfc --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor + { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor +} + +return FIBARO_SMOKE_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua index a4d62fa1a4..bf7d554613 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/fibaro-smoke-sensor/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -24,26 +14,12 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) local FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL = 21600 --seconds -local FIBARO_SMOKE_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x1003 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x3002 }, -- Fibaro Smoke Sensor - { manufacturerId = 0x010F, productType = 0x0C02, productId = 0x4002 } -- Fibaro Smoke Sensor -} --- Determine whether the passed device is fibaro smoke sensro --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is fibaro smoke sensor -local function can_handle_fibaro_smoke_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_SMOKE_SENSOR_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function device_added(self, device) device:send(WakeUp:IntervalSet({node_id = self.environment_info.hub_zwave_id, seconds = FIBARO_SMOKE_SENSOR_WAKEUP_INTERVAL})) @@ -76,7 +52,7 @@ local fibaro_smoke_sensor = { added = device_added }, NAME = "fibaro smoke sensor", - can_handle = can_handle_fibaro_smoke_sensor, + can_handle = require("fibaro-smoke-sensor.can_handle"), health_check = false, } diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua index 0d0a5d1bfd..7968983f63 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -83,12 +73,7 @@ local driver_template = { capabilities.temperatureAlarm, capabilities.temperatureMeasurement }, - sub_drivers = { - require("zwave-smoke-co-alarm-v1"), - require("zwave-smoke-co-alarm-v2"), - require("fibaro-smoke-sensor"), - require("apiv6_bugfix"), - }, + sub_drivers = require("sub_drivers"), lifecycle_handlers = { init = device_init, infoChanged = info_changed, diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..45115081e4 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/lazy_load_subdriver.lua @@ -0,0 +1,18 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +return function(sub_driver_name) + -- gets the current lua libs api version + local ZwaveDriver = require "st.zwave.driver" + local version = require "version" + + if version.api >= 16 then + return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua index 25606a5255..55fa70d48b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/preferences.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local devices = { FIBARO = { diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua new file mode 100644 index 0000000000..f0a2d96412 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/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("zwave-smoke-co-alarm-v1"), + lazy_load_if_possible("zwave-smoke-co-alarm-v2"), + lazy_load_if_possible("fibaro-smoke-sensor"), + lazy_load_if_possible("apiv6_bugfix"), +} +return sub_drivers 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 4f3b08d3f3..dc14f1c51b 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 ebb50eaa6e..0ec85f00f2 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 fd2996b9d1..aa1ebb27f0 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 e42edd4828..1523bf2420 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" 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 95121b1673..4464a10fda 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua new file mode 100644 index 0000000000..dc78d0e4ac --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v1_alarm(opts, driver, device, cmd, ...) + -- The default handlers for the Alarm/Notification command class(es) for the + -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 then + return true, require("zwave-smoke-co-alarm-v1") + end + return false +end + +return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua index 923c516167..8e3cc4e267 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v1/init.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -23,17 +12,6 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0002 -- First Alert Smoke & CO Detector -- manufacturerId = 0x0138, productType = 0x0001, productId = 0x0003 -- First Alert Smoke & CO Detector ---- Determine whether the passed device only supports V1 or V2 of the Alarm command class ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is smoke co alarm -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - -- The default handlers for the Alarm/Notification command class(es) for the - -- Smoke Detector and Carbon Monoxide Detector only handles V3 and up. - return opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version < 3 -end - --- Default handler for alarm command class reports --- --- This converts alarm V1 reports to correct smoke events @@ -75,7 +53,7 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V1", - can_handle = can_handle_v1_alarm, + can_handle = require("zwave-smoke-co-alarm-v1.can_handle"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua new file mode 100644 index 0000000000..6666e7abc2 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_v2_alarm(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2") + end + end + return false +end + +return can_handle_v2_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua new file mode 100644 index 0000000000..baa0d7bbf0 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) + local FINGERPRINTS = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true, require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5") + end + end + return false +end + +return can_handle_fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua new file mode 100644 index 0000000000..37fb1f508c --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FIBARO_CO_SENSORS_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return FIBARO_CO_SENSORS_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua index bde1cbc877..0559b83983 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) @@ -21,10 +11,6 @@ local TAMPERING_AND_EXCEEDING_THE_TEMPERATURE = 3 local ACOUSTIC_SIGNALS = 4 local EXCEEDING_THE_TEMPERATURE = 2 -local FIBARO_CO_SENSORS_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do @@ -40,14 +26,6 @@ end --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_fibaro_co_sensor(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(FIBARO_CO_SENSORS_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local function update_preferences(self, device, args) local preferences = preferencesMap.get_device_parameters(device) @@ -108,7 +86,7 @@ local fibaro_co_sensor = { init = device_init, infoChanged = info_changed }, - can_handle = can_handle_fibaro_co_sensor + can_handle = require("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5.can_handle"), } return fibaro_co_sensor diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua new file mode 100644 index 0000000000..8dc021cc28 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SMOKE_CO_ALARM_V2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor + { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor +} + +return SMOKE_CO_ALARM_V2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua index b49b010cdb..5e69f7108d 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass @@ -20,24 +11,12 @@ local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local SMOKE_CO_ALARM_V2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1000 }, -- Fibaro CO Sensor - { manufacturerId = 0x010F, productType = 0x1201, productId = 0x1001 } -- Fibaro CO Sensor -} --- Determine whether the passed device is Smoke Alarm --- --- @param driver st.zwave.Driver --- @param device st.zwave.Device --- @return boolean true if the device is smoke co alarm -local function can_handle_v2_alarm(opts, driver, device, cmd, ...) - for _, fingerprint in ipairs(SMOKE_CO_ALARM_V2_FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true - end - end - return false -end local device_added = function(self, device) device:emit_event(capabilities.carbonMonoxideDetector.carbonMonoxide.clear()) @@ -94,13 +73,11 @@ local zwave_alarm = { } }, NAME = "Z-Wave smoke and CO alarm V2", - can_handle = can_handle_v2_alarm, + can_handle = require("zwave-smoke-co-alarm-v2.can_handle"), lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("zwave-smoke-co-alarm-v2/fibaro-co-sensor-zw5") - } + sub_drivers = require("zwave-smoke-co-alarm-v2.sub_drivers"), } return zwave_alarm diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua new file mode 100644 index 0000000000..0699743371 --- /dev/null +++ b/drivers/SmartThings/zwave-smoke-alarm/src/zwave-smoke-co-alarm-v2/sub_drivers.lua @@ -0,0 +1,8 @@ +-- 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("zwave-smoke-co-alarm-v2.fibaro-co-sensor-zw5"), +} +return sub_drivers From 8ebd6e44e3dc738948cc8a3815504172216ea14b Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Thu, 19 Feb 2026 15:28:08 -0600 Subject: [PATCH 437/449] add initialPress handling for BILRESA scrolling --- .../src/sub_drivers/ikea_scroll/init.lua | 1 + .../scroll_handlers/event_handlers.lua | 9 +- .../ikea_scroll/scroll_utils/fields.lua | 1 + .../src/test/test_ikea_scroll.lua | 145 +++++++++++++++++- 4 files changed, 145 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua index 5c2a009de1..32b23ccaae 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -49,6 +49,7 @@ local ikea_scroll_handler = { matter_handlers = { event = { [clusters.Switch.ID] = { + [clusters.Switch.events.InitialPress.ID] = event_handlers.initial_press_handler, [clusters.Switch.events.MultiPressOngoing.ID] = event_handlers.multi_press_ongoing_handler, [clusters.Switch.events.MultiPressComplete.ID] = event_handlers.multi_press_complete_handler, } 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 7415da8f9b..da9b1c0392 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 @@ -49,9 +49,12 @@ function IkeaScrollEventHandlers.initial_press_handler(driver, device, ib, respo if switch_utils.tbl_contains(scroll_fields.ENDPOINTS_PUSH, ib.endpoint_id) then generic_event_handlers.initial_press_handler(driver, device, ib, response) else - -- Ignore InitialPress events from non-push endpoints. Presently, we want to solely - -- rely on MultiPressOngoing events to handle rotation for those endpoints. - device.log.debug("Received InitialPress event from scroll endpoint, ignoring.") + -- the magic number "1" occurs in this handler since the InitialPress event represents the first press. + local latest_presses_counted = device:get_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED) or 0 + if latest_presses_counted == 0 then + device:set_field(scroll_fields.LATEST_NUMBER_OF_PRESSES_COUNTED, 1) + rotate_amount_event_helper(device, ib.endpoint_id, 1) + end end end 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 58580328b1..5e98a829c0 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 @@ -37,6 +37,7 @@ IkeaScrollFields.switch_press_subscribed_events = { -- Required Events for the ENDPOINTS_UP_SCROLL and ENDPOINTS_DOWN_SCROLL. Adds a -- MultiPressOngoing subscription to handle step functionality in real-time IkeaScrollFields.switch_scroll_subscribed_events = { + clusters.Switch.events.InitialPress.ID, clusters.Switch.events.MultiPressOngoing.ID, clusters.Switch.events.MultiPressComplete.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 78f99c37fe..4629d69a5f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -133,6 +133,7 @@ local function ikea_scroll_subscribe() clusters.Switch.server.events.MultiPressComplete, } local CLUSTER_SUBSCRIBE_LIST_SCROLL = { + clusters.Switch.events.InitialPress, clusters.Switch.server.events.MultiPressOngoing, clusters.Switch.server.events.MultiPressComplete, } @@ -236,6 +237,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Positive rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -250,7 +267,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(12, {state_change = true})) + capabilities.knob.rotateAmount(6, {state_change = true})) }, { channel = "matter", @@ -279,6 +296,22 @@ test.register_message_test( }, }, { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, + { channel = "matter", direction = "receive", message = { @@ -292,7 +325,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(12, {state_change = true})) + capabilities.knob.rotateAmount(6, {state_change = true})) }, { channel = "matter", @@ -325,6 +358,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Negative rotateAmount events on main are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -339,7 +388,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-12, {state_change = true})) + capabilities.knob.rotateAmount(-6, {state_change = true})) }, { channel = "matter", @@ -367,6 +416,22 @@ test.register_message_test( ) }, }, + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[2], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -381,7 +446,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("main", - capabilities.knob.rotateAmount(-12, {state_change = true})) + capabilities.knob.rotateAmount(-6, {state_change = true})) }, { channel = "matter", @@ -414,6 +479,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Positive rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -428,7 +509,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(12, {state_change = true})) + capabilities.knob.rotateAmount(6, {state_change = true})) }, { channel = "matter", @@ -451,6 +532,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Negative rotateAmount events on group2 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[4], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -465,7 +562,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group2", - capabilities.knob.rotateAmount(-12, {state_change = true})) + capabilities.knob.rotateAmount(-6, {state_change = true})) }, { channel = "matter", @@ -488,6 +585,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Positive rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -502,7 +615,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(12, {state_change = true})) + capabilities.knob.rotateAmount(6, {state_change = true})) }, { channel = "matter", @@ -525,6 +638,22 @@ test.register_message_test( test.register_message_test( "Ikea Scroll Negative rotateAmount events on group3 are emitted correctly", { + { + channel = "matter", + direction = "receive", + message = { + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[6], {new_position = 1} + ) + }, + }, + { + channel = "capability", + direction = "send", + message = mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(-6, {state_change = true})) + }, { channel = "matter", direction = "receive", @@ -539,7 +668,7 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_ikea_scroll:generate_test_message("group3", - capabilities.knob.rotateAmount(-12, {state_change = true})) + capabilities.knob.rotateAmount(-6, {state_change = true})) }, { channel = "matter", From 71e5828e7d37d8cdd87239eff51599f9e8610fe1 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 17 Nov 2025 15:25:04 -0600 Subject: [PATCH 438/449] CHAD-17083: zigbee-window-treatment lazy loading of sub-drivers --- .../src/HOPOsmart/can_handle.lua | 14 + .../src/HOPOsmart/custom_clusters.lua | 16 +- .../src/HOPOsmart/fingerprints.lua | 8 + .../src/HOPOsmart/init.lua | 29 +- .../src/VIVIDSTORM/can_handle.lua | 14 + .../src/VIVIDSTORM/custom_clusters.lua | 16 +- .../src/VIVIDSTORM/fingerprints.lua | 8 + .../src/VIVIDSTORM/init.lua | 29 +- .../src/aqara/can_handle.lua | 14 + .../aqara/curtain-driver-e1/can_handle.lua | 11 + .../src/aqara/curtain-driver-e1/init.lua | 20 +- .../src/aqara/fingerprints.lua | 11 + .../src/aqara/init.lua | 26 +- .../src/aqara/roller-shade/can_handle.lua | 11 + .../src/aqara/roller-shade/init.lua | 283 +++++++++--------- .../src/aqara/sub_drivers.lua | 10 + .../src/aqara/version/can_handle.lua | 13 + .../src/aqara/version/init.lua | 10 +- .../src/axis/axis_version/can_handle.lua | 15 + .../src/axis/axis_version/init.lua | 28 +- .../src/axis/can_handle.lua | 11 + .../zigbee-window-treatment/src/axis/init.lua | 27 +- .../src/axis/sub_drivers.lua | 8 + .../src/feibit/can_handle.lua | 15 + .../src/feibit/fingerprints.lua | 9 + .../src/feibit/init.lua | 31 +- .../src/hanssem/can_handle.lua | 11 + .../src/hanssem/init.lua | 9 +- .../zigbee-window-treatment/src/init.lua | 38 +-- .../src/invert-lift-percentage/can_handle.lua | 14 + .../src/invert-lift-percentage/init.lua | 22 +- .../src/lazy_load_subdriver.lua | 15 + .../src/rooms-beautiful/can_handle.lua | 14 + .../src/rooms-beautiful/fingerprints.lua | 8 + .../src/rooms-beautiful/init.lua | 29 +- .../src/screen-innovations/can_handle.lua | 11 + .../src/screen-innovations/init.lua | 20 +- .../src/somfy/can_handle.lua | 14 + .../src/somfy/fingerprints.lua | 10 + .../src/somfy/init.lua | 31 +- .../src/sub_drivers.lua | 19 ++ .../test_zigbee_window_shade_battery_ikea.lua | 16 +- ...est_zigbee_window_shade_battery_yoolax.lua | 16 +- ...est_zigbee_window_shade_only_HOPOsmart.lua | 16 +- .../src/test/test_zigbee_window_treatment.lua | 16 +- ..._zigbee_window_treatment_VWSDSTUST120H.lua | 16 +- .../test_zigbee_window_treatment_aqara.lua | 16 +- ...ndow_treatment_aqara_curtain_driver_e1.lua | 16 +- ...ow_treatment_aqara_roller_shade_rotate.lua | 16 +- .../test_zigbee_window_treatment_axis.lua | 16 +- .../test_zigbee_window_treatment_feibit.lua | 16 +- .../test_zigbee_window_treatment_hanssem.lua | 18 +- .../test_zigbee_window_treatment_rooms.lua | 16 +- ...ee_window_treatment_screen_innovations.lua | 18 +- .../test_zigbee_window_treatment_somfy.lua | 16 +- .../test_zigbee_window_treatment_vimar.lua | 16 +- .../src/vimar/can_handle.lua | 14 + .../src/vimar/fingerprints.lua | 9 + .../src/vimar/init.lua | 30 +- .../src/window_shade_utils.lua | 18 +- .../src/window_treatment_utils.lua | 16 +- .../src/yoolax/can_handle.lua | 14 + .../src/yoolax/fingerprints.lua | 9 + .../src/yoolax/init.lua | 30 +- 64 files changed, 609 insertions(+), 727 deletions(-) create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/can_handle.lua new file mode 100644 index 0000000000..85db6c0447 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/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("HOPOsmart.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("HOPOsmart") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua index 1f2950b2f1..b0394aa3fe 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/custom_clusters.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua new file mode 100644 index 0000000000..abf5e54ae5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "HOPOsmart", model = "A2230011" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua index 5e3f002ec0..3267eefc33 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/HOPOsmart/init.lua @@ -1,34 +1,13 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local custom_clusters = require "HOPOsmart/custom_clusters" local cluster_base = require "st.zigbee.cluster_base" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "HOPOsmart", model = "A2230011" } -} -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function send_read_attr_request(device, cluster, attr) device:send( @@ -83,7 +62,7 @@ local HOPOsmart_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("HOPOsmart.can_handle"), } return HOPOsmart_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/can_handle.lua new file mode 100644 index 0000000000..d6907690c3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/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("VIVIDSTORM.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("VIVIDSTORM") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua index 6f266a474e..8efbc0e654 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local data_types = require "st.zigbee.data_types" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua new file mode 100644 index 0000000000..ff1bbcb00d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua index ef36757490..106115edee 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zcl_clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" @@ -22,18 +12,7 @@ local MOST_RECENT_SETLEVEL = "windowShade_recent_setlevel" local TIMER = "liftPercentage_timer" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } -} -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function send_read_attr_request(device, cluster, attr) device:send( @@ -156,7 +135,7 @@ local screen_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("VIVIDSTORM.can_handle"), } return screen_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua new file mode 100644 index 0000000000..e4453597ed --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_aqara_products(opts, driver, device) + local FINGERPRINTS = require("aqara.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("aqara") + end + end + return false +end + +return is_aqara_products diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua new file mode 100644 index 0000000000..7eb40a7c10 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function curtain_driver_e1_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.agl001" then + return true, require("aqara.curtain-driver-e1") + end + return false +end + +return curtain_driver_e1_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua index 6fe895ca7d..03e651b679 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/curtain-driver-e1/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -224,9 +214,7 @@ local aqara_curtain_driver_e1_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.agl001" - end + can_handle = require("aqara.curtain-driver-e1.can_handle"), } return aqara_curtain_driver_e1_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua new file mode 100644 index 0000000000..8ad05530a5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/fingerprints.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "LUMI", model = "lumi.curtain" }, + { mfr = "LUMI", model = "lumi.curtain.v1" }, + { mfr = "LUMI", model = "lumi.curtain.aq2" }, + { mfr = "LUMI", model = "lumi.curtain.agl001" } +} + +return FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua index 87505d2e40..1b66236038 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/init.lua @@ -1,3 +1,7 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" @@ -27,21 +31,7 @@ local PREF_SOFT_TOUCH_ON = "\x00\x08\x00\x00\x00\x00\x00" local APPLICATION_VERSION = "application_version" -local FINGERPRINTS = { - { mfr = "LUMI", model = "lumi.curtain" }, - { mfr = "LUMI", model = "lumi.curtain.v1" }, - { mfr = "LUMI", model = "lumi.curtain.aq2" }, - { mfr = "LUMI", model = "lumi.curtain.agl001" } -} -local function is_aqara_products(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 window_shade_level_cmd(driver, device, command) aqara_utils.shade_level_cmd(driver, device, command) @@ -217,12 +207,8 @@ local aqara_window_treatment_handler = { } } }, - sub_drivers = { - require("aqara.roller-shade"), - require("aqara.curtain-driver-e1"), - require("aqara.version") - }, - can_handle = is_aqara_products + sub_drivers = require("aqara.sub_drivers"), + can_handle = require("aqara.can_handle"), } return aqara_window_treatment_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua new file mode 100644 index 0000000000..2e91fa090d --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function roller_shade_can_handle(opts, driver, device, ...) + if device:get_model() == "lumi.curtain.aq2" then + return true, require("aqara.roller-shade") + end + return false +end + +return roller_shade_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua index 12b84eb0c8..909b65dd3e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/roller-shade/init.lua @@ -1,141 +1,142 @@ -local capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" -local data_types = require "st.zigbee.data_types" -local aqara_utils = require "aqara/aqara_utils" -local window_treatment_utils = require "window_treatment_utils" - -local Basic = clusters.Basic -local WindowCovering = clusters.WindowCovering - -local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] -local reverseRollerShadeDir = "stse.reverseRollerShadeDir" -local shadeRotateState = capabilities["stse.shadeRotateState"] -local setRotateStateCommandName = "setRotateState" - -local MULTISTATE_CLUSTER_ID = 0x0013 -local MULTISTATE_ATTRIBUTE_ID = 0x0055 -local ROTATE_UP_VALUE = 0x0004 -local ROTATE_DOWN_VALUE = 0x0005 - - -local function window_shade_level_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - aqara_utils.shade_level_cmd(driver, device, command) - end -end - -local function window_shade_open_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) - end -end - -local function window_shade_close_cmd(driver, device, command) - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) - end -end - -local function set_rotate_command_handler(driver, device, command) - device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI - - -- Cannot be controlled if not initialized - local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, - initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 - if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then - local state = command.args.state - if state == "rotateUp" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - elseif state == "rotateDown" then - local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, - MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) - message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) - device:send(message) - end - end -end - -local function shade_state_report_handler(driver, device, value, zb_rx) - aqara_utils.emit_shade_event_by_state(device, value) -end - -local function pref_report_handler(driver, device, value, zb_rx) - -- initializedState - local initialized = string.byte(value.value, 3) & 0xFF - device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or - initializedStateWithGuide.initializedStateWithGuide.notInitialized()) -end - -local function device_info_changed(driver, device, event, args) - if device.preferences ~= nil then - local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir] - if reverseRollerShadeDirPrefValue ~= nil and - reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir] then - local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, raw_value)) - end - end -end - -local function device_added(driver, device) - device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) - window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) - window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) - device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) - device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) - - device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, - aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) - - -- Initial default settings - device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, - aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) -end - -local aqara_roller_shade_handler = { - NAME = "Aqara Roller Shade Handler", - lifecycle_handlers = { - added = device_added, - infoChanged = device_info_changed - }, - capability_handlers = { - [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd - }, - [capabilities.windowShade.ID] = { - [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, - [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, - }, - [shadeRotateState.ID] = { - [setRotateStateCommandName] = set_rotate_command_handler - } - }, - zigbee_handlers = { - attr = { - [Basic.ID] = { - [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, - [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler - } - } - }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "lumi.curtain.aq2" - end -} - -return aqara_roller_shade_handler +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local FrameCtrl = require "st.zigbee.zcl.frame_ctrl" +local data_types = require "st.zigbee.data_types" +local aqara_utils = require "aqara/aqara_utils" +local window_treatment_utils = require "window_treatment_utils" + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering + +local initializedStateWithGuide = capabilities["stse.initializedStateWithGuide"] +local reverseRollerShadeDir = capabilities["stse.reverseRollerShadeDir"] +local shadeRotateState = capabilities["stse.shadeRotateState"] +local setRotateStateCommandName = "setRotateState" + +local MULTISTATE_CLUSTER_ID = 0x0013 +local MULTISTATE_ATTRIBUTE_ID = 0x0055 +local ROTATE_UP_VALUE = 0x0004 +local ROTATE_DOWN_VALUE = 0x0005 + + +local function window_shade_level_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + aqara_utils.shade_level_cmd(driver, device, command) + end +end + +local function window_shade_open_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 100)) + end +end + +local function window_shade_close_cmd(driver, device, command) + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, 0)) + end +end + +local function set_rotate_command_handler(driver, device, command) + device:emit_event(shadeRotateState.rotateState.idle({state_change = true, visibility = { displayed = false }})) -- update UI + + -- Cannot be controlled if not initialized + local initialized = device:get_latest_state("main", initializedStateWithGuide.ID, + initializedStateWithGuide.initializedStateWithGuide.NAME) or 0 + if initialized == initializedStateWithGuide.initializedStateWithGuide.initialized.NAME then + local state = command.args.state + if state == "rotateUp" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_UP_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + elseif state == "rotateDown" then + local message = cluster_base.write_manufacturer_specific_attribute(device, MULTISTATE_CLUSTER_ID, + MULTISTATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint16, ROTATE_DOWN_VALUE) + message.body.zcl_header.frame_ctrl = FrameCtrl(0x10) + device:send(message) + end + end +end + +local function shade_state_report_handler(driver, device, value, zb_rx) + aqara_utils.emit_shade_event_by_state(device, value) +end + +local function pref_report_handler(driver, device, value, zb_rx) + -- initializedState + local initialized = string.byte(value.value, 3) & 0xFF + device:emit_event(initialized == 1 and initializedStateWithGuide.initializedStateWithGuide.initialized() or + initializedStateWithGuide.initializedStateWithGuide.notInitialized()) +end + +local function device_info_changed(driver, device, event, args) + if device.preferences ~= nil then + local reverseRollerShadeDirPrefValue = device.preferences[reverseRollerShadeDir.ID] + if reverseRollerShadeDirPrefValue ~= nil and + reverseRollerShadeDirPrefValue ~= args.old_st_store.preferences[reverseRollerShadeDir.ID] then + local raw_value = reverseRollerShadeDirPrefValue and aqara_utils.PREF_REVERSE_ON or aqara_utils.PREF_REVERSE_OFF + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, raw_value)) + end + end +end + +local function device_added(driver, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShadeLevel, capabilities.windowShadeLevel.shadeLevel.NAME, capabilities.windowShadeLevel.shadeLevel(0)) + window_treatment_utils.emit_event_if_latest_state_missing(device, "main", capabilities.windowShade, capabilities.windowShade.windowShade.NAME, capabilities.windowShade.windowShade.closed()) + device:emit_event(initializedStateWithGuide.initializedStateWithGuide.notInitialized()) + device:emit_event(shadeRotateState.rotateState.idle({ visibility = { displayed = false }})) + + device:send(cluster_base.write_manufacturer_specific_attribute(device, aqara_utils.PRIVATE_CLUSTER_ID, + aqara_utils.PRIVATE_ATTRIBUTE_ID, aqara_utils.MFG_CODE, data_types.Uint8, 1)) + + -- Initial default settings + device:send(cluster_base.write_manufacturer_specific_attribute(device, Basic.ID, aqara_utils.PREF_ATTRIBUTE_ID, + aqara_utils.MFG_CODE, data_types.CharString, aqara_utils.PREF_REVERSE_OFF)) +end + +local aqara_roller_shade_handler = { + NAME = "Aqara Roller Shade Handler", + lifecycle_handlers = { + added = device_added, + infoChanged = device_info_changed + }, + capability_handlers = { + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.open.NAME] = window_shade_open_cmd, + [capabilities.windowShade.commands.close.NAME] = window_shade_close_cmd, + }, + [shadeRotateState.ID] = { + [setRotateStateCommandName] = set_rotate_command_handler + } + }, + zigbee_handlers = { + attr = { + [Basic.ID] = { + [aqara_utils.SHADE_STATE_ATTRIBUTE_ID] = shade_state_report_handler, + [aqara_utils.PREF_ATTRIBUTE_ID] = pref_report_handler + } + } + }, + can_handle = require("aqara.roller-shade.can_handle"), +} + +return aqara_roller_shade_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua new file mode 100644 index 0000000000..297f29d970 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/sub_drivers.lua @@ -0,0 +1,10 @@ +-- 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("aqara.roller-shade"), + lazy_load_if_possible("aqara.curtain-driver-e1"), + lazy_load_if_possible("aqara.version"), +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua new file mode 100644 index 0000000000..5d9f7f0135 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function version_can_handle(opts, driver, device) + local APPLICATION_VERSION = "application_version" + local softwareVersion = device:get_field(APPLICATION_VERSION) + if softwareVersion and softwareVersion ~= 34 then + return true, require("aqara.version") + end + return false +end + +return version_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua index 10182ee928..ae1887d79a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/aqara/version/init.lua @@ -1,9 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local clusters = require "st.zigbee.zcl.clusters" local WindowCovering = clusters.WindowCovering -local APPLICATION_VERSION = "application_version" - local function shade_level_report_legacy_handler(driver, device, value, zb_rx) -- not implemented end @@ -17,10 +18,7 @@ local aqara_window_treatment_version_handler = { } } }, - can_handle = function(opts, driver, device) - local softwareVersion = device:get_field(APPLICATION_VERSION) - return softwareVersion and softwareVersion ~= 34 - end + can_handle = require("aqara.version.can_handle"), } return aqara_window_treatment_version_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua new file mode 100644 index 0000000000..828eb170fa --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_axis_gear_version = function(opts, driver, device) + local SOFTWARE_VERSION = "software_version" + local MIN_WINDOW_COVERING_VERSION = 1093 + local version = device:get_field(SOFTWARE_VERSION) or 0 + + if version >= MIN_WINDOW_COVERING_VERSION then + return true, require("axis.axis_version") + end + return false +end + +return is_axis_gear_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua index c0682d73c0..cbb2930bc3 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/axis_version/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local window_shade_utils = require "window_shade_utils" @@ -22,18 +12,8 @@ local Level = zcl_clusters.Level local PowerConfiguration = zcl_clusters.PowerConfiguration local WindowCovering = zcl_clusters.WindowCovering -local SOFTWARE_VERSION = "software_version" -local MIN_WINDOW_COVERING_VERSION = 1093 local DEFAULT_LEVEL = 0 -local is_axis_gear_version = function(opts, driver, device) - local version = device:get_field(SOFTWARE_VERSION) or 0 - - if version >= MIN_WINDOW_COVERING_VERSION then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -141,7 +121,7 @@ local axis_handler_version = { } } }, - can_handle = is_axis_gear_version, + can_handle = require("axis.axis_version.can_handle"), } return axis_handler_version diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua new file mode 100644 index 0000000000..049e47acb5 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + if device:get_manufacturer() == "AXIS" then + return true, require("axis") + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua index 612dd450a5..ec7c96b975 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/init.lua @@ -1,16 +1,7 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" @@ -26,12 +17,6 @@ local WindowCovering = zcl_clusters.WindowCovering local SOFTWARE_VERSION = "software_version" local DEFAULT_LEVEL = 0 -local is_zigbee_window_shade = function(opts, driver, device) - if device:get_manufacturer() == "AXIS" then - return true - end - return false -end -- Commands local function window_shade_set_level(device, command, level) @@ -151,8 +136,8 @@ local axis_handler = { added = device_added, doConfigure = do_configure, }, - sub_drivers = { require("axis.axis_version") }, - can_handle = is_zigbee_window_shade, + sub_drivers = require("axis.sub_drivers"), + can_handle = require("axis.can_handle"), } return axis_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua new file mode 100644 index 0000000000..e3ea740478 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/axis/sub_drivers.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require("lazy_load_subdriver") + +return { + lazy_load_if_possible("axis.axis_version") +} diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua new file mode 100644 index 0000000000..32457dde8a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/can_handle.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("feibit.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("feibit") + end + end + + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua new file mode 100644 index 0000000000..95781ff992 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, + { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua index e0fd17219e..1e97fbc4ce 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/feibit/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -19,20 +9,7 @@ local window_shade_defaults = require "st.zigbee.defaults.windowShade_defaults" local device_management = require "st.zigbee.device_management" local Level = zcl_clusters.Level -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.6" }, - { mfr = "Feibit Co.Ltd", model = "FTB56-ZT218AK1.8" }, -} - -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function set_shade_level(device, value, component) local level = math.floor(value / 100.0 * 254) @@ -87,7 +64,7 @@ local feibit_handler = { lifecycle_handlers = { doConfigure = do_configure, }, - can_handle = is_zigbee_window_shade, + can_handle = require("feibit.can_handle"), } return feibit_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua new file mode 100644 index 0000000000..65f1a6dc75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function hanssem_can_handle(opts, driver, device, ...) + if device:get_model() == "TS0601" then + return true, require("hanssem") + end + return false +end + +return hanssem_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua index be956d79b2..41b4eb1cac 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/hanssem/init.lua @@ -1,3 +1,6 @@ +-- Copyright 2021-2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- -- Based on https://github.com/iquix/ST-Edge-Driver/blob/master/tuya-window-shade/src/init.lua -- Copyright 2021-2022 Jaewon Park (iquix) @@ -241,9 +244,7 @@ local hanssem_window_treatment = { added = device_added, infoChanged = device_info_changed }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "TS0601" - end + can_handle = require("hanssem.can_handle"), } -return hanssem_window_treatment \ No newline at end of file +return hanssem_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 3403b3d528..16783a8726 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local ZigbeeDriver = require "st.zigbee" @@ -46,29 +36,17 @@ local zigbee_window_treatment_driver_template = { capabilities.powerSource, capabilities.battery }, - sub_drivers = { - require("vimar"), - require("aqara"), - require("feibit"), - require("somfy"), - require("invert-lift-percentage"), - require("rooms-beautiful"), - require("axis"), - require("yoolax"), - require("hanssem"), - require("screen-innovations"), - require("VIVIDSTORM"), - require("HOPOsmart")}, - lifecycle_handlers = { - init = init_handler, - added = added_handler - }, capability_handlers = { [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, } }, + lifecycle_handlers = { + init = init_handler, + added = added_handler + }, + sub_drivers = require("sub_drivers"), health_check = false, } diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua new file mode 100644 index 0000000000..69cfd4393a --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function invert_lift_percentage_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "IKEA of Sweden" or + device:get_manufacturer() == "Smartwings" or + device:get_manufacturer() == "Insta GmbH" + then + return true, require("invert-lift-percentage") + end + return false +end + +return invert_lift_percentage_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua index 19532bc847..b586459b9a 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/invert-lift-percentage/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -97,11 +87,7 @@ local ikea_window_treatment = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd } }, - can_handle = function(opts, driver, device, ...) - return device:get_manufacturer() == "IKEA of Sweden" or - device:get_manufacturer() == "Smartwings" or - device:get_manufacturer() == "Insta GmbH" - end + can_handle = require("invert-lift-percentage.can_handle"), } return ikea_window_treatment diff --git a/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua new file mode 100644 index 0000000000..0bee6d2a75 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/lazy_load_subdriver.lua @@ -0,0 +1,15 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + local ZigbeeDriver = require "st.zigbee" + if version.api >= 16 then + return ZigbeeDriver.lazy_load_sub_driver_v2(sub_driver_name) + elseif version.api >= 9 then + return ZigbeeDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/can_handle.lua new file mode 100644 index 0000000000..6bc25d2f91 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/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("rooms-beautiful.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("rooms-beautiful") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua new file mode 100644 index 0000000000..71ece32b8e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Rooms Beautiful", model = "C001" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua index fc4883aa7f..bb868a8716 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/rooms-beautiful/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -21,22 +11,11 @@ local PowerConfiguration = zcl_clusters.PowerConfiguration local OnOff = zcl_clusters.OnOff local WindowCovering = zcl_clusters.WindowCovering -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Rooms Beautiful", model = "C001" } -} local INVERT_CLUSTER = 0xFC00 local INVERT_CLUSTER_ATTRIBUTE = 0x0000 local PREV_TIME = "shadeLevelCmdTime" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function invert_preference_handler(device) local window_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 @@ -129,7 +108,7 @@ local rooms_beautiful_handler = { init = battery_defaults.build_linear_voltage_init(2.5, 3.0), infoChanged = info_changed }, - can_handle = is_zigbee_window_shade, + can_handle = require("rooms-beautiful.can_handle"), } return rooms_beautiful_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua new file mode 100644 index 0000000000..df291c2612 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function screen_innovations_can_handle(opts, driver, device, ...) + if device:get_model() == "WM25/L-Z" then + return true, require("screen-innovations") + end + return false +end + +return screen_innovations_can_handle diff --git a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua index 868e92af56..49397a5369 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/screen-innovations/init.lua @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- require st provided libraries local capabilities = require "st.capabilities" @@ -173,9 +163,7 @@ local screeninnovations_roller_shade_handler = { } } }, - can_handle = function(opts, driver, device, ...) - return device:get_model() == "WM25/L-Z" - end + can_handle = require("screen-innovations.can_handle"), } -- return the handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/can_handle.lua new file mode 100644 index 0000000000..27a6c83ca3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/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("somfy.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("somfy") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua new file mode 100644 index 0000000000..ce6094564c --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/fingerprints.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, + { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, + { mfr = "SOMFY", model = "Sonesse 40 Roller" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua index ffc9541b64..da416ba9ea 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/somfy/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -20,23 +10,10 @@ local WindowCovering = zcl_clusters.WindowCovering local GLYDEA_MOVE_THRESHOLD = 3 -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "SOMFY", model = "Glydea Ultra Curtain" }, - { mfr = "SOMFY", model = "Sonesse 30 WF Roller" }, - { mfr = "SOMFY", model = "Sonesse 40 Roller" } -} local MOVE_LESS_THAN_THRESHOLD = "_sameLevelEvent" local FINAL_STATE_POLL_TIMER = "_finalStatePollTimer" -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function overwrite_existing_timer_if_needed(device, new_timer) local old_timer = device:get_field(FINAL_STATE_POLL_TIMER) @@ -132,7 +109,7 @@ local somfy_handler = { } } }, - can_handle = is_zigbee_window_shade, + can_handle = require("somfy.can_handle"), } return somfy_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua new file mode 100644 index 0000000000..959c8d8c22 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua @@ -0,0 +1,19 @@ +-- 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("vimar"), + lazy_load_if_possible("aqara"), + lazy_load_if_possible("feibit"), + lazy_load_if_possible("somfy"), + lazy_load_if_possible("invert-lift-percentage"), + lazy_load_if_possible("rooms-beautiful"), + lazy_load_if_possible("axis"), + lazy_load_if_possible("yoolax"), + lazy_load_if_possible("hanssem"), + lazy_load_if_possible("screen-innovations"), + lazy_load_if_possible("VIVIDSTORM"), + lazy_load_if_possible("HOPOsmart"), +} +return sub_drivers 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 c0e83e5ab8..a73bb6c5a3 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua index d51f303378..c13164df09 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 1e4a8b3a2a..4c3028fd46 100755 --- 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 6380e5ce51..16d7ebf366 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 394498b3c8..69da00efb8 100755 --- 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 @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" 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 d669681c0b..050d0b34f0 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" 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 2b095c6c16..ea389680f2 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local clusters = require "st.zigbee.zcl.clusters" 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 a27f85f528..bd9d5684e6 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local base64 = require "st.base64" local capabilities = require "st.capabilities" 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 f3564ff79f..e8faf2a33d 100755 --- 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua index aa0480eb2a..7cfb443256 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_feibit.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua index 2e511c99bb..db50f28d32 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_hanssem.lua @@ -1,16 +1,6 @@ --- Copyright 2023 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. +-- Copyright 2023 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" @@ -371,4 +361,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 b3bc0b6c29..303397191d 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local base64 = require "st.base64" 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 b7f630cf71..c0a004ff6e 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 @@ -1,16 +1,6 @@ --- Copyright 2024 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. +-- Copyright 2024 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" @@ -429,4 +419,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() 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 4c66d29257..6c8ea27d01 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 @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua index 80a6552d8b..ae87438c99 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + -- Mock out globals local test = require "integration_test" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/can_handle.lua new file mode 100644 index 0000000000..1d72817eae --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/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("vimar.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("vimar") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua new file mode 100644 index 0000000000..ea7f4cd3bf --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Vimar", model = "Window_Cov_v1.0" }, + { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua index 9fa928645a..dd5ea15aed 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/vimar/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local utils = require "st.utils" @@ -27,20 +17,8 @@ local windowShade = capabilities.windowShade.windowShade local VIMAR_SHADES_OPENING = "_vimarShadesOpening" local VIMAR_SHADES_CLOSING = "_vimarShadesClosing" -local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Vimar", model = "Window_Cov_v1.0" }, - { mfr = "Vimar", model = "Window_Cov_Module_v1.0" } -} -- UTILS to check manufacturer details -local is_zigbee_window_shade = function(opts, driver, device) - for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end -- ATTRIBUTE HANDLER FOR CurrentPositionLiftPercentage local function current_position_attr_handler(driver, device, value, zb_rx) @@ -176,7 +154,7 @@ local vimar_handler = { lifecycle_handlers = { init = device_init }, - can_handle = is_zigbee_window_shade, + can_handle = require("vimar.can_handle"), } return vimar_handler 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 262e549c2d..f3e09c20a6 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -41,4 +31,4 @@ utils.set_preset_position_cmd = function(driver, device, command) device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) end -return utils \ No newline at end of file +return utils diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua index 2f20ff2b4f..5b2ec304e6 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_treatment_utils.lua @@ -1,16 +1,6 @@ --- Copyright 2025 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. +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local window_treatment_utils = {} diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua new file mode 100644 index 0000000000..006fa3e1bb --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function is_yoolax_window_shade(opts, driver, device) + local FINGERPRINTS = require("yoolax.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("yoolax") + end + end + return false +end + +return is_yoolax_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua new file mode 100644 index 0000000000..30e0dd4c62 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/fingerprints.lua @@ -0,0 +1,9 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment + { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment +} + +return YOOLAX_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua index 46cb33fed2..5a593cdf2c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/yoolax/init.lua @@ -1,16 +1,6 @@ --- Copyright 2022 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. +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -24,19 +14,7 @@ local device_management = require "st.zigbee.device_management" local LEVEL_UPDATE_TIMEOUT = "__level_update_timeout" local MOST_RECENT_SETLEVEL = "__most_recent_setlevel" -local YOOLAX_WINDOW_SHADE_FINGERPRINTS = { - { mfr = "Yookee", model = "D10110" }, -- Yookee Window Treatment - { mfr = "yooksmart", model = "D10110" } -- yooksmart Window Treatment -} -local function is_yoolax_window_shade(opts, driver, device) - for _, fingerprint in ipairs(YOOLAX_WINDOW_SHADE_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true - end - end - return false -end local function default_response_handler(driver, device, zb_message) local is_success = zb_message.body.zcl_body.status.value @@ -160,7 +138,7 @@ local yoolax_window_shade = { } }, }, - can_handle = is_yoolax_window_shade + can_handle = require("yoolax.can_handle"), } return yoolax_window_shade From 344f87fb9b3916b22795d50591dd480e317d8da7 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 23 Feb 2026 11:56:04 -0600 Subject: [PATCH 439/449] Revert "CHAD-17092: zwave-sensor lazy loading of sub-drivers" This reverts commit 9a714175757dea23539df997566506121325e578. --- .../src/aeotec-multisensor/can_handle.lua | 15 ------ .../src/aeotec-multisensor/fingerprints.lua | 9 ---- .../src/aeotec-multisensor/init.lua | 37 ++++++++++++-- .../multisensor-6/can_handle.lua | 11 ---- .../aeotec-multisensor/multisensor-6/init.lua | 23 +++++++-- .../multisensor-7/can_handle.lua | 12 ----- .../aeotec-multisensor/multisensor-7/init.lua | 24 +++++++-- .../src/aeotec-multisensor/sub_drivers.lua | 9 ---- .../src/aeotec-water-sensor/can_handle.lua | 15 ------ .../src/aeotec-water-sensor/fingerprints.lua | 11 ---- .../src/aeotec-water-sensor/init.lua | 34 +++++++++++-- .../src/apiv6_bugfix/can_handle.lua | 35 ------------- .../zwave-sensor/src/apiv6_bugfix/init.lua | 33 ++++++++++-- .../zwave-sensor/src/configurations.lua | 16 ++++-- .../src/enerwave-motion-sensor/can_handle.lua | 12 ----- .../src/enerwave-motion-sensor/init.lua | 27 ++++++++-- .../can_handle.lua | 17 ------- .../everspring-motion-light-sensor/init.lua | 32 ++++++++++-- .../can_handle.lua | 15 ------ .../ezmultipli-multipurpose-sensor/init.lua | 32 +++++++++--- .../fibaro-door-window-sensor/can_handle.lua | 15 ------ .../can_handle.lua | 14 ------ .../fingerprints.lua | 8 --- .../fibaro-door-window-sensor-1/init.lua | 30 +++++++++-- .../can_handle.lua | 14 ------ .../fingerprints.lua | 10 ---- .../fibaro-door-window-sensor-2/init.lua | 32 ++++++++++-- .../fingerprints.lua | 15 ------ .../src/fibaro-door-window-sensor/init.lua | 43 ++++++++++++++-- .../fibaro-door-window-sensor/sub_drivers.lua | 9 ---- .../src/fibaro-flood-sensor/can_handle.lua | 14 ------ .../src/fibaro-flood-sensor/init.lua | 30 +++++++++-- .../src/fibaro-motion-sensor/can_handle.lua | 15 ------ .../src/fibaro-motion-sensor/init.lua | 29 +++++++++-- .../src/firmware-version/can_handle.lua | 21 -------- .../src/firmware-version/init.lua | 34 +++++++++++-- .../can_handle.lua | 20 -------- .../glentronics-water-leak-sensor/init.lua | 36 +++++++++++-- .../src/homeseer-multi-sensor/can_handle.lua | 20 -------- .../src/homeseer-multi-sensor/init.lua | 36 +++++++++++-- drivers/SmartThings/zwave-sensor/src/init.lua | 50 +++++++++++++++++-- .../zwave-sensor/src/lazy_load_subdriver.lua | 18 ------- .../zwave-sensor/src/preferences.lua | 16 ++++-- .../src/sensative-strip/can_handle.lua | 14 ------ .../zwave-sensor/src/sensative-strip/init.lua | 27 ++++++++-- .../zwave-sensor/src/sub_drivers.lua | 26 ---------- .../src/test/test_aeon_multisensor.lua | 16 ++++-- .../src/test/test_aeotec_multisensor_6.lua | 16 ++++-- .../src/test/test_aeotec_multisensor_7.lua | 16 ++++-- .../src/test/test_aeotec_multisensor_gen5.lua | 16 ++++-- .../src/test/test_aeotec_water_sensor.lua | 16 ++++-- .../src/test/test_aeotec_water_sensor_7.lua | 16 ++++-- .../src/test/test_enerwave_motion_sensor.lua | 16 ++++-- .../src/test/test_everpsring_sp817.lua | 16 ++++-- .../src/test/test_everspring_PIR_sensor.lua | 16 ++++-- .../src/test/test_everspring_ST814.lua | 16 ++++-- .../test_everspring_illuminance_sensor.lua | 16 ++++-- .../test_everspring_motion_light_sensor.lua | 16 ++++-- .../test_ezmultipli_multipurpose_sensor.lua | 16 ++++-- .../test/test_fibaro_door_window_sensor.lua | 16 ++++-- .../test/test_fibaro_door_window_sensor_1.lua | 16 ++++-- .../test/test_fibaro_door_window_sensor_2.lua | 16 ++++-- ...ro_door_window_sensor_with_temperature.lua | 16 ++++-- .../src/test/test_fibaro_flood_sensor.lua | 16 ++++-- .../src/test/test_fibaro_flood_sensor_zw5.lua | 16 ++++-- .../src/test/test_fibaro_motion_sensor.lua | 16 ++++-- .../test/test_fibaro_motion_sensor_zw5.lua | 16 ++++-- .../src/test/test_generic_sensor.lua | 16 ++++-- .../test_glentronics_water_leak_sensor.lua | 16 ++++-- .../src/test/test_homeseer_multi_sensor.lua | 16 ++++-- .../src/test/test_no_wakeup_poll.lua | 18 +++++-- .../src/test/test_sensative_strip.lua | 16 ++++-- .../test_smartthings_water_leak_sensor.lua | 16 ++++-- .../src/test/test_v1_contact_event.lua | 16 ++++-- .../src/test/test_vision_motion_detector.lua | 16 ++++-- .../src/test/test_zooz_4_in_1_sensor.lua | 16 ++++-- .../test/test_zwave_motion_light_sensor.lua | 16 ++++-- .../test_zwave_motion_temp_light_sensor.lua | 16 ++++-- .../src/test/test_zwave_sensor.lua | 16 ++++-- .../src/test/test_zwave_water_sensor.lua | 16 ++++-- .../src/timed-tamper-clear/can_handle.lua | 23 --------- .../src/timed-tamper-clear/init.lua | 36 +++++++++++-- .../src/v1-contact-event/can_handle.lua | 22 -------- .../src/v1-contact-event/init.lua | 31 ++++++++++-- .../src/vision-motion-detector/can_handle.lua | 17 ------- .../src/vision-motion-detector/init.lua | 33 ++++++++++-- .../src/wakeup-no-poll/can_handle.lua | 12 ----- .../zwave-sensor/src/wakeup-no-poll/init.lua | 32 +++++++++--- .../src/zooz-4-in-1-sensor/can_handle.lua | 15 ------ .../src/zooz-4-in-1-sensor/fingerprints.lua | 10 ---- .../src/zooz-4-in-1-sensor/init.lua | 33 ++++++++++-- .../zwave-water-leak-sensor/can_handle.lua | 15 ------ .../zwave-water-leak-sensor/fingerprints.lua | 19 ------- .../src/zwave-water-leak-sensor/init.lua | 43 ++++++++++++++-- 94 files changed, 1160 insertions(+), 742 deletions(-) delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/sub_drivers.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua deleted file mode 100644 index 99cea3c0b3..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_aeotec_multisensor(opts, self, device, ...) - local FINGERPRINTS = require("aeotec-multisensor.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-multisensor") - return true, subdriver, require("aeotec-multisensor") - end - end - return false -end - -return can_handle_aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua deleted file mode 100644 index 9436a85979..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/fingerprints.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local AEOTEC_MULTISENSOR_FINGERPRINTS = { - { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 - { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 -} - -return AEOTEC_MULTISENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua index d1759a41e0..edd01c7553 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/init.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -7,6 +18,21 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local AEOTEC_MULTISENSOR_FINGERPRINTS = { + { manufacturerId = 0x0086, productId = 0x0064 }, -- MultiSensor 6 + { manufacturerId = 0x0371, productId = 0x0018 }, -- MultiSensor 7 +} + +local function can_handle_aeotec_multisensor(opts, self, device, ...) + for _, fingerprint in ipairs(AEOTEC_MULTISENSOR_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-multisensor") + return true, subdriver + end + end + return false +end + local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -35,9 +61,12 @@ local aeotec_multisensor = { [Notification.REPORT] = notification_report_handler } }, - sub_drivers = require("aeotec-multisensor.sub_drivers"), + sub_drivers = { + require("aeotec-multisensor/multisensor-6"), + require("aeotec-multisensor/multisensor-7") + }, NAME = "aeotec multisensor", - can_handle = require("aeotec-multisensor.can_handle"), + can_handle = can_handle_aeotec_multisensor } return aeotec_multisensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua deleted file mode 100644 index d86e9c8b3a..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/can_handle.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_multisensor_6(opts, self, device, ...) -local MULTISENSOR_6_PRODUCT_ID = 0x0064 - if device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID then - return true, require("aeotec-multisensor.multisensor-6") - end - return false -end -return can_handle_multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua index 4174b3b14e..1b9d4d6b97 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-6/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -10,8 +19,12 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version = 2}) +local MULTISENSOR_6_PRODUCT_ID = 0x0064 local PREFERENCE_NUM = 9 +local function can_handle_multisensor_6(opts, self, device, ...) + return device.zwave_product_id == MULTISENSOR_6_PRODUCT_ID +end local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -49,7 +62,7 @@ local multisensor_6 = { } }, NAME = "aeotec multisensor 6", - can_handle = require("aeotec-multisensor.multisensor-6.can_handle"), + can_handle = can_handle_multisensor_6 } return multisensor_6 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua deleted file mode 100644 index f109d0e31c..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/can_handle.lua +++ /dev/null @@ -1,12 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_multisensor_7(opts, self, device, ...) - local MULTISENSOR_7_PRODUCT_ID = 0x0018 - if device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID then - return true, require("aeotec-multisensor.multisensor-7") - end - return false -end - -return can_handle_multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua index c3dc69178f..2d2bf4e36e 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/multisensor-7/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -10,8 +19,13 @@ local cc = require "st.zwave.CommandClass" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 2 }) +local MULTISENSOR_7_PRODUCT_ID = 0x0018 local PREFERENCE_NUM = 10 +local function can_handle_multisensor_7(opts, self, device, ...) + return device.zwave_product_id == MULTISENSOR_7_PRODUCT_ID +end + local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -48,7 +62,7 @@ local multisensor_7 = { } }, NAME = "aeotec multisensor 7", - can_handle = require("aeotec-multisensor.multisensor-7.can_handle"), + can_handle = can_handle_multisensor_7 } return multisensor_7 diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua deleted file mode 100644 index 396f53fe86..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-multisensor/sub_drivers.lua +++ /dev/null @@ -1,9 +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("aeotec-multisensor/multisensor-6"), - lazy_load_if_possible("aeotec-multisensor/multisensor-7"), -} -return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua deleted file mode 100644 index 1bdf9f12a8..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) - local FINGERPRINTS = require("aeotec-water-sensor.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("aeotec-water-sensor") - return true, subdriver, require("aeotec-water-sensor") - end - end - return false -end - -return can_handle_zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua deleted file mode 100644 index 423d87754e..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/fingerprints.lua +++ /dev/null @@ -1,11 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { - { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU - { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US - { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU - { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 -} - -return ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua index 4c7a86e708..9d883ea3c2 100644 --- a/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/aeotec-water-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -9,8 +18,23 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS = { + { manufacturerId = 0x0371, productType = 0x0002, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro EU + { manufacturerId = 0x0371, productType = 0x0102, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro US + { manufacturerId = 0x0371, productType = 0x0202, productId = 0x0013 }, -- Aeotec Water Sensor 7 Pro AU + { manufacturerId = 0x0371, productId = 0x0012 } -- Aeotec Water Sensor 7 +} --- Determine whether the passed device is zwave water temperature humidiry sensor +local function can_handle_zwave_water_temp_humidity_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(ZWAVE_WATER_TEMP_HUMIDITY_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("aeotec-water-sensor") + return true, subdriver + end + end + return false +end --- Default handler for notification command class reports --- @@ -44,7 +68,7 @@ local zwave_water_temp_humidity_sensor = { }, }, NAME = "zwave water temp humidity sensor", - can_handle = require("aeotec-water-sensor.can_handle"), + can_handle = can_handle_zwave_water_temp_humidity_sensor } return zwave_water_temp_humidity_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 4913e9a25e..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,35 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - --- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll -local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor -local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat -local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 -local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 -local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor -local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 -local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} -local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, - AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, - ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - - for _, fp in ipairs(FPS) do - if device:id_match(fp.mfr, fp.prod, fp.model) then return false end - end - local subdriver = require("apiv6_bugfix") - return true, subdriver - else - return false - end -end - -return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua index 94dc5975ab..322333d565 100644 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua @@ -1,9 +1,34 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - local cc = require "st.zwave.CommandClass" local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) +-- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll +local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor +local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat +local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 +local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 +local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor +local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 +local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} +local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, + AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, + ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} + +local function can_handle(opts, driver, device, cmd, ...) + local version = require "version" + if version.api == 6 and + cmd.cmd_class == cc.WAKE_UP and + cmd.cmd_id == WakeUp.NOTIFICATION then + + for _, fp in ipairs(FPS) do + if device:id_match(fp.mfr, fp.prod, fp.model) then return false end + end + local subdriver = require("apiv6_bugfix") + return true, subdriver + else + return false + end +end + local function wakeup_notification(driver, device, cmd) device:refresh() end @@ -15,7 +40,7 @@ local apiv6_bugfix = { } }, NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), + can_handle = can_handle } return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-sensor/src/configurations.lua b/drivers/SmartThings/zwave-sensor/src/configurations.lua index 0a3c62ead8..2883e70384 100644 --- a/drivers/SmartThings/zwave-sensor/src/configurations.lua +++ b/drivers/SmartThings/zwave-sensor/src/configurations.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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" --- @type st.zwave.CommandClass.Configuration diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua deleted file mode 100644 index a707d7493a..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/can_handle.lua +++ /dev/null @@ -1,12 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) - local ENERWAVE_MFR = 0x011A - if device.zwave_manufacturer_id == ENERWAVE_MFR then - local subdriver = require("enerwave-motion-sensor") - return true, subdriver, require("enerwave-motion-sensor") - else return false end -end - -return can_handle_enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua index 012a8884a3..6fb712e3b0 100644 --- a/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/enerwave-motion-sensor/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -10,6 +20,15 @@ local Association = (require "st.zwave.CommandClass.Association")({version=2}) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({version=1}) +local ENERWAVE_MFR = 0x011A + +local function can_handle_enerwave_motion_sensor(opts, driver, device, cmd, ...) + if device.zwave_manufacturer_id == ENERWAVE_MFR then + local subdriver = require("enerwave-motion-sensor") + return true, subdriver + else return false end +end + local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher --This is done to help the hub correctly set the checkInterval for migrated devices. @@ -39,7 +58,7 @@ local enerwave_motion_sensor = { doConfigure = do_configure }, NAME = "enerwave_motion_sensor", - can_handle = require("enerwave-motion-sensor.can_handle") + can_handle = can_handle_enerwave_motion_sensor } return enerwave_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua deleted file mode 100644 index c9fd2eafd1..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/can_handle.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_everspring_motion_light(opts, driver, device, ...) - local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } - if device:id_match( - EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, - EVERSPRING_MOTION_LIGHT_FINGERPRINT.model - ) then - local subdriver = require("everspring-motion-light-sensor") - return true, subdriver - end - return false -end - -return can_handle_everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua index 8baa3756d6..1b11aadabe 100644 --- a/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/everspring-motion-light-sensor/init.lua @@ -1,12 +1,34 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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 SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2,strict=true}) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version=2}) +local EVERSPRING_MOTION_LIGHT_FINGERPRINT = { mfr = 0x0060, prod = 0x0012, model = 0x0001 } + +local function can_handle_everspring_motion_light(opts, driver, device, ...) + if device:id_match( + EVERSPRING_MOTION_LIGHT_FINGERPRINT.mfr, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.prod, + EVERSPRING_MOTION_LIGHT_FINGERPRINT.model + ) then + local subdriver = require("everspring-motion-light-sensor") + return true, subdriver + else return false end +end + local function device_added(driver, device) device:emit_event(capabilities.motionSensor.motion.inactive()) device:send(SwitchBinary:Get({})) @@ -18,7 +40,7 @@ local everspring_motion_light = { lifecycle_handlers = { added = device_added }, - can_handle = require("everspring-motion-light-sensor.can_handle"), + can_handle = can_handle_everspring_motion_light } return everspring_motion_light diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua deleted file mode 100644 index 5596894fbb..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) - local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } - if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, - EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then - local subdriver = require("ezmultipli-multipurpose-sensor") - return true, subdriver - end - return false -end - -return can_handle_ezmultipli_multipurpose_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua index f4cac30faa..1e4b3bf0ce 100644 --- a/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/ezmultipli-multipurpose-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.utils @@ -19,6 +28,17 @@ local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({version=2}) local CAP_CACHE_KEY = "st.capabilities." .. capabilities.colorControl.ID +local EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0004, productId = 0x0001 } + +local function can_handle_ezmultipli_multipurpose_sensor(opts, driver, device, ...) + if device:id_match(EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.manufacturerId, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productType, + EZMULTIPLI_MULTIPURPOSE_SENSOR_FINGERPRINTS.productId) then + local subdriver = require("ezmultipli-multipurpose-sensor") + return true, subdriver + else return false end +end + local function basic_report_handler(driver, device, cmd) local event local value = (cmd.args.target_value ~= nil) and cmd.args.target_value or cmd.args.value @@ -82,7 +102,7 @@ local ezmultipli_multipurpose_sensor = { [capabilities.colorControl.commands.setColor.NAME] = set_color } }, - can_handle = require("ezmultipli-multipurpose-sensor.can_handle"), + can_handle = can_handle_ezmultipli_multipurpose_sensor } -return ezmultipli_multipurpose_sensor +return ezmultipli_multipurpose_sensor \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua deleted file mode 100644 index c088fd2b2f..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) - local FINGERPRINTS = require("fibaro-door-window-sensor.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then - local subdriver = require("fibaro-door-window-sensor") - return true, subdriver, require("fibaro-door-window-sensor") - end - end - return false -end - -return can_handle_fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/can_handle.lua deleted file mode 100644 index 992ea8fd9d..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/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_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) - local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-1") - end - end - return false -end - -return can_handle_fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua deleted file mode 100644 index 50727133bb..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -} - -return FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua index 4dbca58919..698fffcceb 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-1/init.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 cc = require "st.zwave.CommandClass" @@ -10,6 +21,19 @@ local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 1 }) local configurationsMap = require "configurations" +local FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } +} + +local function can_handle_fibaro_door_window_sensor_1(opts, driver, device, cmd, ...) + for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_1_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true + end + end + return false +end + local function sensor_alarm_report_handler(driver, device, cmd) if (cmd.args.sensor_state == SensorAlarm.sensor_state.ALARM) then device:emit_event(capabilities.tamperAlert.tamper.detected()) @@ -68,7 +92,7 @@ local fibaro_door_window_sensor_1 = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh }, - can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-1.can_handle"), + can_handle = can_handle_fibaro_door_window_sensor_1 } return fibaro_door_window_sensor_1 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/can_handle.lua deleted file mode 100644 index 4493496f94..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/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_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) - local FINGERPRINTS = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true, require("fibaro-door-window-sensor.fibaro-door-window-sensor-2") - end - end - return false -end - -return can_handle_fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua deleted file mode 100644 index 6103c107d1..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/fingerprints.lua +++ /dev/null @@ -1,10 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ -} - -return FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua index 250203b0cd..16c5ec2017 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fibaro-door-window-sensor-2/init.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -7,6 +18,21 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Alarm local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 2 }) +local FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS = { + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, productType = 0x0702, productId = 0x3000 } -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ +} + +local function can_handle_fibaro_door_window_sensor_2(opts, driver, device, cmd, ...) + for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_2_FINGERPRINTS) do + if device:id_match( fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + return true + end + end + return false +end + local function emit_event_if_latest_state_missing(device, component, capability, attribute_name, value) if device:get_latest_state(component, capability.ID, attribute_name) == nil then device:emit_event(value) @@ -57,7 +83,7 @@ local fibaro_door_window_sensor_2 = { lifecycle_handlers = { added = device_added }, - can_handle = require("fibaro-door-window-sensor.fibaro-door-window-sensor-2.can_handle"), + can_handle = can_handle_fibaro_door_window_sensor_2, } return fibaro_door_window_sensor_2 diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua deleted file mode 100644 index 699df3f623..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/fingerprints.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe - { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA - { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA - { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor - { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor -} - -return FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua index 6c30508fd1..86cf865348 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/init.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 cc = require "st.zwave.CommandClass" local capabilities = require "st.capabilities" @@ -13,6 +24,27 @@ local preferencesMap = require "preferences" local FIBARO_DOOR_WINDOW_SENSOR_WAKEUP_INTERVAL = 21600 --seconds +local FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS = { + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x1000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / Europe + { manufacturerId = 0x010F, prod = 0x0700, productId = 0x2000 }, -- Fibaro Open/Closed Sensor (FGK-10x) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x1000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / Europe + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x2000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / NA + { manufacturerId = 0x010F, prod = 0x0702, productId = 0x3000 }, -- Fibaro Open/Closed Sensor 2 (FGDW-002) / ANZ + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x2001 }, -- Fibaro Open/Closed Sensor with temperature (FGK-10X) / NA + { manufacturerId = 0x010F, prod = 0x0701, productId = 0x1001 }, -- Fibaro Open/Closed Sensor + { manufacturerId = 0x010F, prod = 0x0501, productId = 0x1002 } -- Fibaro Open/Closed Sensor +} + +local function can_handle_fibaro_door_window_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(FIBARO_DOOR_WINDOW_SENSOR_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.prod, fingerprint.productId) then + local subdriver = require("fibaro-door-window-sensor") + return true, subdriver + end + end + return false +end + local function parameterNumberToParameterName(preferences,parameterNumber) for id, parameter in pairs(preferences) do if parameter.parameter_number == parameterNumber then @@ -122,8 +154,11 @@ local fibaro_door_window_sensor = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } }, - sub_drivers = require("fibaro-door-window-sensor.sub_drivers"), - can_handle = require("fibaro-door-window-sensor.can_handle"), + sub_drivers = { + require("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), + require("fibaro-door-window-sensor/fibaro-door-window-sensor-2") + }, + can_handle = can_handle_fibaro_door_window_sensor } return fibaro_door_window_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua deleted file mode 100644 index 0c4ddd4e43..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-door-window-sensor/sub_drivers.lua +++ /dev/null @@ -1,9 +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("fibaro-door-window-sensor/fibaro-door-window-sensor-1"), - lazy_load_if_possible("fibaro-door-window-sensor/fibaro-door-window-sensor-2"), -} -return sub_drivers diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/can_handle.lua deleted file mode 100644 index 341fbcd6f9..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/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_fibaro_flood_sensor(opts, driver, device, ...) - local FIBARO_MFR_ID = 0x010F - local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } - if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then - local subdriver = require("fibaro-flood-sensor") - return true, subdriver - end - return false -end - -return can_handle_fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua index 9a0a8e7eaa..144be985ae 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-flood-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -17,6 +26,17 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = local preferences = require "preferences" local configurations = require "configurations" +local FIBARO_MFR_ID = 0x010F +local FIBARO_FLOOD_PROD_TYPES = { 0x0000, 0x0B00 } + +local function can_handle_fibaro_flood_sensor(opts, driver, device, ...) + if device:id_match(FIBARO_MFR_ID, FIBARO_FLOOD_PROD_TYPES, nil) then + local subdriver = require("fibaro-flood-sensor") + return true, subdriver + else return false end +end + + local function basic_set_handler(self, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -76,7 +96,7 @@ local fibaro_flood_sensor = { lifecycle_handlers = { doConfigure = do_configure }, - can_handle = require("fibaro-flood-sensor.can_handle"), + can_handle = can_handle_fibaro_flood_sensor } return fibaro_flood_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua deleted file mode 100644 index cd3f647394..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) - - local FIBARO_MOTION_MFR = 0x010F - local FIBARO_MOTION_PROD = 0x0800 - if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then - local subdriver = require("fibaro-motion-sensor") - return true, subdriver, require("fibaro-motion-sensor") - end - return false -end - -return can_handle_fibaro_motion_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua index ed035bde18..ae45f5a27b 100644 --- a/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/fibaro-motion-sensor/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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. --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -8,6 +18,15 @@ local cc = require "st.zwave.CommandClass" local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 }) local capabilities = require "st.capabilities" +local FIBARO_MOTION_MFR = 0x010F +local FIBARO_MOTION_PROD = 0x0800 + +local function can_handle_fibaro_motion_sensor(opts, driver, device, ...) + if device:id_match(FIBARO_MOTION_MFR, FIBARO_MOTION_PROD) then + local subdriver = require("fibaro-motion-sensor") + return true, subdriver + else return false end +end local function sensor_alarm_report(driver, device, cmd) if (cmd.args.sensor_state ~= SensorAlarm.sensor_state.NO_ALARM) then @@ -24,7 +43,7 @@ local fibaro_motion_sensor = { [SensorAlarm.REPORT] = sensor_alarm_report } }, - can_handle = require("fibaro-motion-sensor.can_handle") + can_handle = can_handle_fibaro_motion_sensor } -return fibaro_motion_sensor +return fibaro_motion_sensor \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua deleted file mode 100644 index 3ecdc2baf0..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/firmware-version/can_handle.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" - ---This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled -local FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor -} - -return function(opts, driver, device, ...) - if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subDriver = require("firmware-version") - return true, subDriver - end - end - end - return false -end \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua index 528cf7da45..058a7f955c 100644 --- a/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/firmware-version/init.lua @@ -1,6 +1,16 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2025 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" --- @type st.zwave.CommandClass @@ -10,6 +20,22 @@ local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) +--This sub_driver will populate the currentVersion (firmware) when the firmwareUpdate capability is enabled +local FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x7000, productId = 0xE002 } -- Zooz ZSE42 Water Sensor +} + +local function can_handle_fw(opts, driver, device, ...) + if device:supports_capability_by_id(capabilities.firmwareUpdate.ID) then + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subDriver = require("firmware-version") + return true, subDriver + end + end + end + return false +end --Runs upstream handlers (ex zwave_handlers) local function call_parent_handler(handlers, self, device, event, args) @@ -47,7 +73,7 @@ end local firmware_version = { NAME = "firmware_version", - can_handle = require("firmware-version.can_handle"), + can_handle = can_handle_fw, lifecycle_handlers = { added = added_handler, diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua deleted file mode 100644 index e24d7b9cf2..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/can_handle.lua +++ /dev/null @@ -1,20 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - ---- Determine whether the passed device is glentronics water leak sensor ---- ---- @param driver Driver driver instance ---- @param device Device device isntance ---- @return boolean true if the device proper, else false -local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) - local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor - if device:id_match( - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, - GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then - return true, require("glentronics-water-leak-sensor") - end - return false -end - -return can_handle_glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua index 7400d889b7..3dba7351d6 100644 --- a/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/glentronics-water-leak-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -9,6 +18,23 @@ local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS = { manufacturerId = 0x0084, productType = 0x0093, productId = 0x0114 } -- glentronics water leak sensor + +--- Determine whether the passed device is glentronics water leak sensor +--- +--- @param driver Driver driver instance +--- @param device Device device isntance +--- @return boolean true if the device proper, else false +local function can_handle_glentronics_water_leak_sensor(opts, driver, device, ...) + if device:id_match( + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.manufacturerId, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productType, + GLENTRONICS_WATER_LEAK_SENSOR_FINGERPRINTS.productId) then + local subdriver = require("glentronics-water-leak-sensor") + return true, subdriver + else return false end +end + local function notification_report_handler(self, device, cmd) local event if cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then @@ -52,7 +78,7 @@ local glentronics_water_leak_sensor = { added = device_added }, NAME = "glentronics water leak sensor", - can_handle = require("glentronics-water-leak-sensor.can_handle"), + can_handle = can_handle_glentronics_water_leak_sensor } return glentronics_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua deleted file mode 100644 index 992c1f7c7f..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/can_handle.lua +++ /dev/null @@ -1,20 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - ---- Determine whether the passed device is homeseer multi sensor ---- ---- @param driver Driver driver instance ---- @param device Device device instance ---- @return boolean true if the device proper, else false -local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) - local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 - if device:id_match( - HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, - HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then - return true, require("homeseer-multi-sensor") - end - return false -end - -return can_handle_homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua index f89e6a7870..2330f28106 100644 --- a/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/homeseer-multi-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -13,6 +22,23 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({version = 5}) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1}) +local HOMESEER_MULTI_SENSOR_FINGERPRINTS = { manufacturerId = 0x001E, productType = 0x0002, productId = 0x0001 } -- Homeseer multi sensor HSM100 + +--- Determine whether the passed device is homeseer multi sensor +--- +--- @param driver Driver driver instance +--- @param device Device device instance +--- @return boolean true if the device proper, else false +local function can_handle_homeseer_multi_sensor(opts, driver, device, ...) + if device:id_match( + HOMESEER_MULTI_SENSOR_FINGERPRINTS.manufacturerId, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productType, + HOMESEER_MULTI_SENSOR_FINGERPRINTS.productId) then + local subdriver = require("homeseer-multi-sensor") + return true, subdriver + else return false end +end + local function basic_set_handler(self, device, cmd) if cmd.args.value ~= nil then device:emit_event(cmd.args.value == 0xFF and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) @@ -61,7 +87,7 @@ local homeseer_multi_sensor = { init = device_init, }, NAME = "homeseer multi sensor", - can_handle = require("homeseer-multi-sensor.can_handle"), + can_handle = can_handle_homeseer_multi_sensor } return homeseer_multi_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/init.lua b/drivers/SmartThings/zwave-sensor/src/init.lua index 2c18e4c2ed..213aa8c389 100644 --- a/drivers/SmartThings/zwave-sensor/src/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/init.lua @@ -1,5 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -16,6 +27,19 @@ local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) local preferences = require "preferences" local configurations = require "configurations" +local function lazy_load_if_possible(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + + -- version 9 will include the lazy loading functions + if version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end + +end + --- Handle preference changes --- --- @param driver st.zwave.Driver @@ -110,7 +134,27 @@ local driver_template = { capabilities.powerMeter, capabilities.smokeDetector }, - sub_drivers = require("sub_drivers"), + sub_drivers = { + lazy_load_if_possible("zooz-4-in-1-sensor"), + lazy_load_if_possible("vision-motion-detector"), + lazy_load_if_possible("fibaro-flood-sensor"), + lazy_load_if_possible("aeotec-water-sensor"), + lazy_load_if_possible("glentronics-water-leak-sensor"), + lazy_load_if_possible("homeseer-multi-sensor"), + lazy_load_if_possible("fibaro-door-window-sensor"), + lazy_load_if_possible("sensative-strip"), + lazy_load_if_possible("enerwave-motion-sensor"), + lazy_load_if_possible("aeotec-multisensor"), + lazy_load_if_possible("zwave-water-leak-sensor"), + lazy_load_if_possible("everspring-motion-light-sensor"), + lazy_load_if_possible("ezmultipli-multipurpose-sensor"), + lazy_load_if_possible("fibaro-motion-sensor"), + lazy_load_if_possible("v1-contact-event"), + lazy_load_if_possible("timed-tamper-clear"), + lazy_load_if_possible("wakeup-no-poll"), + lazy_load_if_possible("firmware-version"), + lazy_load_if_possible("apiv6_bugfix"), + }, lifecycle_handlers = { added = added_handler, init = device_init, diff --git a/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua b/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua deleted file mode 100644 index 45115081e4..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/lazy_load_subdriver.lua +++ /dev/null @@ -1,18 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - -return function(sub_driver_name) - -- gets the current lua libs api version - local ZwaveDriver = require "st.zwave.driver" - local version = require "version" - - if version.api >= 16 then - return ZwaveDriver.lazy_load_sub_driver_v2(sub_driver_name) - elseif version.api >= 9 then - return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) - else - return require(sub_driver_name) - end - -end diff --git a/drivers/SmartThings/zwave-sensor/src/preferences.lua b/drivers/SmartThings/zwave-sensor/src/preferences.lua index 70293b10fa..9585b6ffe9 100644 --- a/drivers/SmartThings/zwave-sensor/src/preferences.lua +++ b/drivers/SmartThings/zwave-sensor/src/preferences.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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. --- @type st.zwave.CommandClass.Configuration local Configuration = (require "st.zwave.CommandClass.Configuration")({ version=4 }) diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/can_handle.lua deleted file mode 100644 index e639c32c94..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/sensative-strip/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_sensative_strip(opts, driver, device, cmd, ...) - local SENSATIVE_MFR = 0x019A - local SENSATIVE_MODEL = 0x000A - if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then - local subdriver = require("sensative-strip") - return true, subdriver, require("sensative-strip") - end - return false -end - -return can_handle_sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua index 7fd9fb2258..73f1cb8459 100644 --- a/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/sensative-strip/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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. --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -9,11 +19,20 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.WakeUp local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) +local SENSATIVE_MFR = 0x019A +local SENSATIVE_MODEL = 0x000A local LEAKAGE_ALARM_PARAM = 12 local LEAKAGE_ALARM_OFF = 0 local SENSATIVE_COMFORT_PROFILE = "illuminance-temperature" local CONFIG_REPORT_RECEIVED = "configReportReceived" +local function can_handle_sensative_strip(opts, driver, device, cmd, ...) + if device:id_match(SENSATIVE_MFR, nil, SENSATIVE_MODEL) then + local subdriver = require("sensative-strip") + return true, subdriver + else return false end +end + local function configuration_report(driver, device, cmd) local parameter_number = cmd.args.parameter_number local configuration_value = cmd.args.configuration_value @@ -56,7 +75,7 @@ local sensative_strip = { doConfigure = do_configure }, NAME = "sensative_strip", - can_handle = require("sensative-strip.can_handle") + can_handle = can_handle_sensative_strip } return sensative_strip diff --git a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua deleted file mode 100644 index 9504479304..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua +++ /dev/null @@ -1,26 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require("lazy_load_subdriver") - -return { - lazy_load_if_possible("zooz-4-in-1-sensor"), - lazy_load_if_possible("vision-motion-detector"), - lazy_load_if_possible("fibaro-flood-sensor"), - lazy_load_if_possible("aeotec-water-sensor"), - lazy_load_if_possible("glentronics-water-leak-sensor"), - lazy_load_if_possible("homeseer-multi-sensor"), - lazy_load_if_possible("fibaro-door-window-sensor"), - lazy_load_if_possible("sensative-strip"), - lazy_load_if_possible("enerwave-motion-sensor"), - lazy_load_if_possible("aeotec-multisensor"), - lazy_load_if_possible("zwave-water-leak-sensor"), - lazy_load_if_possible("everspring-motion-light-sensor"), - lazy_load_if_possible("ezmultipli-multipurpose-sensor"), - lazy_load_if_possible("fibaro-motion-sensor"), - lazy_load_if_possible("v1-contact-event"), - lazy_load_if_possible("timed-tamper-clear"), - lazy_load_if_possible("wakeup-no-poll"), - lazy_load_if_possible("firmware-version"), - lazy_load_if_possible("apiv6_bugfix"), -} 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 cfdb08f89d..2d83fd6e25 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 e78e63e214..02c2adf751 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 5bbd1b4f0f..7fd57e42b2 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 57584e779c..3b73ff17c3 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 e58badc224..8af011f51f 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 eb951cef9b..a5f44ff12e 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 bf2d800817..a90655669d 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 87d87b75b5..45e0672c11 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 c6192bcddf..1cb2735a31 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 c576543902..097016fc81 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 f6db164cfe..04103db0db 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 6e04140010..17a627a736 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 2e0ead247a..74b6d6196c 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 d3f51b0bb5..248c22ab60 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 d597b72bdc..6904f318a9 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 2bbe391a62..bc78bd02ca 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 0fd2d2c8df..2b27d66868 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 98edcbf9f6..7a9743a69e 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 0ad90d3479..45cde29b8f 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 6d1f459361..24a7f31eaf 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 4b322b315d..e514a2f2e5 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 950da6f412..e6fff57b0d 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 5ba9ee537e..747c4cbce1 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 52a273f0b7..f57bfe7950 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 65f30fb7a8..9818f1422c 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 @@ -1,6 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2023 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 test = require "integration_test" local capabilities = require "st.capabilities" @@ -103,4 +113,4 @@ test.register_message_test( } ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file 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 a5be01cb81..873909df23 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local zw = require "st.zwave" 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 1b89db9f9f..2321b5bd09 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 b4de025737..866508e3d6 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 @@ -1,6 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2023 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 1cf3da742a..e1b82a6b38 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 c0cc183c41..1bef24c5d6 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 4b149c9be7..4ff11090c9 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 2256522888..d918f47179 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 3719a31f1c..a390c35db9 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" 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 2a8eea8eab..e6caf1f07c 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 @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 test = require "integration_test" local capabilities = require "st.capabilities" diff --git a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua deleted file mode 100644 index c05cbdcf7d..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/can_handle.lua +++ /dev/null @@ -1,23 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_tamper_event(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) - local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F - - if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and - opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or - cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then - return true, require("timed-tamper-clear") - end - return false -end - -return can_handle_tamper_event 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 c2420791b5..2007bedb0d 100644 --- a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua @@ -1,7 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2023 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. --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -11,6 +20,23 @@ local capabilities = require "st.capabilities" local TAMPER_TIMER = "_tamper_timer" local TAMPER_CLEAR = 10 +local FIBARO_DOOR_WINDOW_MFR_ID = 0x010F + +local function can_handle_tamper_event(opts, driver, device, cmd, ...) + if device.zwave_manufacturer_id ~= FIBARO_DOOR_WINDOW_MFR_ID and + opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + (cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_COVER_REMOVED or + cmd.args.event == Notification.event.home_security.TAMPERING_PRODUCT_MOVED) then + local subdriver = require("timed-tamper-clear") + return true, subdriver + else return false + end +end -- This behavior is from zwave-door-window-sensor.groovy. We've seen this behavior -- in Ecolink and several other z-wave sensors that do not send tamper clear events @@ -34,7 +60,7 @@ local timed_tamper_clear = { } }, NAME = "timed tamper clear", - can_handle = require("timed-tamper-clear.can_handle"), + can_handle = can_handle_tamper_event } return timed_tamper_clear diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua deleted file mode 100644 index 2fa45d8015..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/can_handle.lua +++ /dev/null @@ -1,22 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) - - if opts.dispatcher_class == "ZwaveDispatcher" and - cmd ~= nil and - cmd.cmd_class ~= nil and - cmd.cmd_class == cc.NOTIFICATION and - cmd.cmd_id == Notification.REPORT and - cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and - cmd.args.v1_alarm_type == 0x07 then - local subdriver = require("v1-contact-event") - return true, subdriver, require("v1-contact-event") - else - return false - end -end - -return can_handle_v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua index 887ac32bf0..40efc7633e 100644 --- a/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/v1-contact-event/init.lua @@ -1,5 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2023 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. --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -7,6 +18,20 @@ local cc = require "st.zwave.CommandClass" local Notification = (require "st.zwave.CommandClass.Notification")({ version = 4 }) local capabilities = require "st.capabilities" +local function can_handle_v1_contact_event(opts, driver, device, cmd, ...) + if opts.dispatcher_class == "ZwaveDispatcher" and + cmd ~= nil and + cmd.cmd_class ~= nil and + cmd.cmd_class == cc.NOTIFICATION and + cmd.cmd_id == Notification.REPORT and + cmd.args.notification_type == Notification.notification_type.HOME_SECURITY and + cmd.args.v1_alarm_type == 0x07 then + local subdriver = require("v1-contact-event") + return true, subdriver + else + return false + end +end -- This behavior is from zwave-door-window-sensor.groovy, where it is -- indicated that certain monoprice sensors had this behavior. Also, @@ -28,7 +53,7 @@ local v1_contact_event = { } }, NAME = "v1 contact event", - can_handle = require("v1-contact-event.can_handle"), + can_handle = can_handle_v1_contact_event } return v1_contact_event diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua deleted file mode 100644 index d270a7954d..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/can_handle.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - ---- Determine whether the passed device is zwave-plus-motion-temp-sensor -local function can_handle_vision_motion_detector(opts, driver, device, ...) - local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 - if device:id_match( - VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, - VISION_MOTION_DETECTOR_FINGERPRINTS.productType, - VISION_MOTION_DETECTOR_FINGERPRINTS.productId - ) then - return true, require("vision-motion-detector") - end - return false -end - -return can_handle_vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua index 72934362c5..320bf3824f 100644 --- a/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/vision-motion-detector/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -13,6 +22,20 @@ local Configuration = (require "st.zwave.CommandClass.Configuration")({ version --- @type st.zwave.CommandClass.Notification local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local VISION_MOTION_DETECTOR_FINGERPRINTS = { manufacturerId = 0x0109, productType = 0x2002, productId = 0x0205 } -- Vision Motion Detector ZP3102 + +--- Determine whether the passed device is zwave-plus-motion-temp-sensor +local function can_handle_vision_motion_detector(opts, driver, device, ...) + if device:id_match( + VISION_MOTION_DETECTOR_FINGERPRINTS.manufacturerId, + VISION_MOTION_DETECTOR_FINGERPRINTS.productType, + VISION_MOTION_DETECTOR_FINGERPRINTS.productId + ) then + local subdriver = require("vision-motion-detector") + return true, subdriver + else return false end +end + --- Handler for notification report command class from sensor --- --- @param self st.zwave.Driver @@ -60,7 +83,7 @@ local vision_motion_detector = { doConfigure = do_configure, }, NAME = "Vision motion detector", - can_handle = require("vision-motion-detector.can_handle"), + can_handle = can_handle_vision_motion_detector } return vision_motion_detector diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua deleted file mode 100644 index 15ac66d439..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/can_handle.lua +++ /dev/null @@ -1,12 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, ...) - local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - return true, require("wakeup-no-poll") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua index 1270cd4557..59d298a0e4 100644 --- a/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/wakeup-no-poll/init.lua @@ -1,7 +1,16 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2023 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" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -12,6 +21,17 @@ local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({version = 2 --- @type st.zwave.CommandClass.Battery local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local fingerprint = {manufacturerId = 0x014F, productType = 0x2001, productId = 0x0102} -- NorTek open/close sensor + +local function can_handle(opts, driver, device, ...) + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("wakeup-no-poll") + return true, subdriver + else + return false + end +end + -- Nortek open/closed sensors _always_ respond with "open" when polled, and they are polled after wakeup local function wakeup_notification(driver, device, cmd) --Note sending WakeUpIntervalGet the first time a device wakes up will happen by default in Lua libs 0.49.x and higher @@ -33,7 +53,7 @@ local wakeup_no_poll = { [WakeUp.NOTIFICATION] = wakeup_notification } }, - can_handle = require("wakeup-no-poll.can_handle"), + can_handle = can_handle } -return wakeup_no_poll +return wakeup_no_poll \ No newline at end of file diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua deleted file mode 100644 index 17c80b70fb..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) - local FINGERPRINTS = require("zooz-4-in-1-sensor.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then - local subdriver = require("zooz-4-in-1-sensor") - return true, subdriver, require("zooz-4-in-1-sensor") - end - end - return false -end - -return can_handle_zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua deleted file mode 100644 index 12d853b147..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/fingerprints.lua +++ /dev/null @@ -1,10 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local ZOOZ_4_IN_1_FINGERPRINTS = { - { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor - { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion - { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 -} - -return ZOOZ_4_IN_1_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua index 7321c3f9b4..5d4570e525 100644 --- a/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zooz-4-in-1-sensor/init.lua @@ -1,7 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - +-- Copyright 2022 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" --- @type st.zwave.CommandClass @@ -13,8 +22,22 @@ local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ ve --- @type st.utils local utils = require "st.utils" +local ZOOZ_4_IN_1_FINGERPRINTS = { + { manufacturerId = 0x027A, productType = 0x2021, productId = 0x2101 }, -- Zooz 4-in-1 sensor + { manufacturerId = 0x0109, productType = 0x2021, productId = 0x2101 }, -- ZP3111US 4-in-1 Motion + { manufacturerId = 0x0060, productType = 0x0001, productId = 0x0004 } -- Everspring Immune Pet PIR Sensor SP815 +} --- Determine whether the passed device is zooz_4_in_1_sensor +local function can_handle_zooz_4_in_1_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(ZOOZ_4_IN_1_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + local subdriver = require("zooz-4-in-1-sensor") + return true, subdriver + end + end + return false +end --- Handler for notification report command class --- @@ -86,7 +109,7 @@ local zooz_4_in_1_sensor = { } }, NAME = "zooz 4 in 1 sensor", - can_handle = require("zooz-4-in-1-sensor.can_handle"), + can_handle = can_handle_zooz_4_in_1_sensor } return zooz_4_in_1_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua deleted file mode 100644 index ad7e736a7c..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/can_handle.lua +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_water_leak_sensor(opts, driver, device, ...) - local FINGERPRINTS = require("zwave-water-leak-sensor.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then - local subdriver = require("zwave-water-leak-sensor") - return true, subdriver, require("zwave-water-leak-sensor") - end - end - return false -end - -return can_handle_water_leak_sensor diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua deleted file mode 100644 index c07bdb52cb..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/fingerprints.lua +++ /dev/null @@ -1,19 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local WATER_LEAK_SENSOR_FINGERPRINTS = { - {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor - {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor - {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor - {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor - {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) - {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) - {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) - {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor - {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector - {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor -} - -return WATER_LEAK_SENSOR_FINGERPRINTS diff --git a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua index 4575de352a..1eefab7479 100644 --- a/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/zwave-water-leak-sensor/init.lua @@ -1,10 +1,47 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 +-- Copyright 2022 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 cc = require "st.zwave.CommandClass" local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) + +local WATER_LEAK_SENSOR_FINGERPRINTS = { + {mfr = 0x0084, prod = 0x0063, model = 0x010C}, -- SmartThings Water Leak Sensor + {mfr = 0x0084, prod = 0x0053, model = 0x0216}, -- FortrezZ Water Leak Sensor + {mfr = 0x021F, prod = 0x0003, model = 0x0085}, -- Dome Leak Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x0085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x1085}, -- NEO Coolcam Water Sensor + {mfr = 0x0258, prod = 0x0003, model = 0x2085}, -- NEO Coolcam Water Sensor + {mfr = 0x0086, prod = 0x0002, model = 0x007A}, -- Aeotec Water Sensor 6 (EU) + {mfr = 0x0086, prod = 0x0102, model = 0x007A}, -- Aeotec Water Sensor 6 (US) + {mfr = 0x0086, prod = 0x0202, model = 0x007A}, -- Aeotec Water Sensor 6 (AU) + {mfr = 0x000C, prod = 0x0201, model = 0x000A}, -- HomeSeer LS100+ Water Sensor + {mfr = 0x0173, prod = 0x4C47, model = 0x4C44}, -- Leak Gopher Z-Wave Leak Detector + {mfr = 0x027A, prod = 0x7000, model = 0xE002} -- Zooz ZSE42 XS Water Leak Sensor +} + +local function can_handle_water_leak_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(WATER_LEAK_SENSOR_FINGERPRINTS) do + if device:id_match(fingerprint.mfr, fingerprint.prod, fingerprint.model) then + local subdriver = require("zwave-water-leak-sensor") + return true, subdriver + end + end + return false +end + local function basic_set_handler(driver, device, cmd) local value = cmd.args.target_value and cmd.args.target_value or cmd.args.value device:emit_event(value == 0xFF and capabilities.waterSensor.water.wet() or capabilities.waterSensor.water.dry()) @@ -17,7 +54,7 @@ local water_leak_sensor = { [Basic.SET] = basic_set_handler } }, - can_handle = require("zwave-water-leak-sensor.can_handle"), + can_handle = can_handle_water_leak_sensor } return water_leak_sensor From a2a8611ea9c97430f8fecfd0f5fa994f4e337b1c Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 20 Feb 2026 14:43:23 -0600 Subject: [PATCH 440/449] Increase Z-Wave driver coverage Add comments about aeotec doorbell siren bugs --- .../src/test/test_zwave_multi_button.lua | 26 ++ .../src/test/test_aeon_multisensor.lua | 24 ++ .../src/test/test_aeotec_multisensor_6.lua | 22 ++ .../src/test/test_fibaro_flood_sensor.lua | 29 ++ .../src/test/test_firmware_version.lua | 33 +++ .../src/test/test_zooz_4_in_1_sensor.lua | 18 ++ .../src/test/test_zwave_sensor.lua | 36 +++ .../src/aeotec-doorbell-siren/init.lua | 15 +- .../src/test/test_fibaro_roller_shutter.lua | 26 ++ .../src/test/test_qubino_flush_shutter.lua | 255 ++++++++++-------- .../test_zwave_iblinds_window_treatment.lua | 48 ++++ .../src/test/test_zwave_window_treatment.lua | 39 +++ 12 files changed, 461 insertions(+), 110 deletions(-) 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 27c9b851f9..3ddf585681 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 @@ -864,4 +864,30 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Central scene notification with scene_number beyond profile buttons falls back to main component", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_aeotec_wallmote_quad.id, + zw_test_utils.zwave_test_build_receive_command( + CentralScene:Notification({ key_attributes = CentralScene.key_attributes.KEY_PRESSED_1_TIME, scene_number = 5 }) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_aeotec_wallmote_quad:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + }, + { + channel = "capability", + direction = "send", + message = mock_aeotec_wallmote_quad:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + } + } +) + test.run_registered_tests() 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 2d83fd6e25..59435aa68a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -15,7 +15,9 @@ local test = require "integration_test" local zw = require "st.zwave" local zw_test_utils = require "integration_test.zwave_test_utils" +local capabilities = require "st.capabilities" local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) @@ -92,4 +94,26 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Notification HOME_SECURITY MOTION_DETECTION should be handled as motion active", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_sensor.id, + zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.MOTION_DETECTION + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + test.run_registered_tests() 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 02c2adf751..f858438d9a 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 @@ -413,4 +413,26 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Notification HOME_SECURITY MOTION_DETECTION should be handled as motion active", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_sensor.id, + zw_test_utils.zwave_test_build_receive_command(Notification:Report({ + notification_type = Notification.notification_type.HOME_SECURITY, + event = Notification.event.home_security.MOTION_DETECTION + })) + } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + test.run_registered_tests() 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 7a9743a69e..2d93278b47 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 @@ -20,6 +20,8 @@ local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) local SensorAlarm = (require "st.zwave.CommandClass.SensorAlarm")({ version = 1 }) local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) local SensorMultilevel = (require "st.zwave.CommandClass.SensorMultilevel")({ version = 5 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 4 }) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) local t_utils = require "integration_test.utils" local sensor_endpoints = { @@ -277,4 +279,31 @@ test.register_coroutine_test( ) +test.register_coroutine_test( + "doConfigure should call initial_configuration and preferences for non-wakeup device", + function () + test.socket.zwave:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_sensor.id, "doConfigure" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Configuration:Set({ parameter_number = 74, configuration_value = 3, size = 1 }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Association:Set({ grouping_identifier = 2, node_ids = {} }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_sensor, + Association:Set({ grouping_identifier = 3, node_ids = {} }) + ) + ) + mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() 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 9790b5cfb5..d4903fc30d 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua @@ -17,6 +17,7 @@ local test = require "integration_test" local cc = require "st.zwave.CommandClass" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass.Version local Version = (require "st.zwave.CommandClass.Version")({ version = 1 }) --- @type st.zwave.CommandClass.WakeUp @@ -88,4 +89,36 @@ test.register_message_test( } ) +test.register_message_test( + "Version:Report should emit firmwareUpdate.currentVersion", + { + { + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(Version:Report({ + application_version = 1, + application_sub_version = 5, + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.firmwareUpdate.currentVersion({ value = "1.05" })) + } + } +) + +test.register_coroutine_test( + "added lifecycle event should emit initial state and request firmware version", + function () + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.waterSensor.water.dry()) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command(mock_device, Version:Get({})) + ) + end +) + test.run_registered_tests() \ No newline at end of file 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 1bef24c5d6..66cca4afe4 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 @@ -228,4 +228,22 @@ test.register_message_test( } ) +test.register_message_test( + "Sensor multilevel luminance report with value=0 uses default lux conversion", + { + { + channel = "zwave", + direction = "receive", + message = { mock_sensor.id, zw_test_utils.zwave_test_build_receive_command(SensorMultilevel:Report({ + sensor_type = SensorMultilevel.sensor_type.LUMINANCE, + sensor_value = 0 })) } + }, + { + channel = "capability", + direction = "send", + message = mock_sensor:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({value = 0, unit = "lux"})) + } + } +) + test.run_registered_tests() 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 a390c35db9..c15daeb188 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -735,4 +735,40 @@ test.register_message_test( } ) +test.register_message_test( + "Basic Set value=0 for contact sensor should emit contact.closed", + { + { + channel = "zwave", + direction = "receive", + message = { mock_contact_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({ + value = 0 + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_contact_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "Basic Set value=0 for motion sensor should emit motion.inactive", + { + { + channel = "zwave", + direction = "receive", + message = { mock_motion_device.id, zw_test_utils.zwave_test_build_receive_command(Basic:Set({ + value = 0 + })) } + }, + { + channel = "capability", + direction = "send", + message = mock_motion_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua index 35f006e94f..716c46186e 100644 --- a/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua +++ b/drivers/SmartThings/zwave-siren/src/aeotec-doorbell-siren/init.lua @@ -33,7 +33,6 @@ local BUTTON_BATTERY_NORMAL = 99 local DEVICE_PROFILE_CHANGE_IN_PROGRESS = "device_profile_change_in_progress" local NEXT_BUTTON_BATTERY_EVENT_DETAILS = "next_button_battery_event_details" - local function querySoundStatus(device) for endpoint = 2, NUMBER_OF_SOUND_COMPONENTS do device:send_to_component(Basic:Get({}), "sound"..endpoint) @@ -208,6 +207,7 @@ local function changeDeviceProfileIfNeeded(device, endpoint) end end +-- Note that endpoint should be a number not a dst_channels table. local function setActiveEndpoint(device, endpoint) if (endpoint) then device:set_field(LAST_TRIGGERED_ENDPOINT, endpoint, {persist = true}) @@ -271,20 +271,22 @@ end local function alarmChimeOnOff(device, command, newValue) if (device and command and newValue) then + -- Note that zwave/device.lua send_to_component expects the component_to_endpoint function to + -- return a dst_channels table, not a single endpoint number local endpoint = component_to_endpoint(device, command.component) - device:send(Basic:Set({value = newValue})):to_endpoint(endpoint) + device:send_to_component(Basic:Set({value = newValue}), command.component) if (newValue == ON) then - setActiveEndpoint(endpoint) + setActiveEndpoint(device, endpoint[1]) end end end -local function alarm_chime_on(device, command) +local function alarm_chime_on(self, device, command) resetActiveEndpoint(device) alarmChimeOnOff(device, command, ON) end -local function alarm_chime_off(device, command) +local function alarm_chime_off(self, device, command) alarmChimeOnOff(device, command, OFF) end @@ -306,6 +308,9 @@ local aeotec_doorbell_siren = { [Notification.REPORT] = notification_report_handler } }, + -- This typo is a bug. There are many unit tests that fail, when + -- it is enabled, and it is not clear what the correct functionality is + -- without real device testing. capabilities_handlers = { [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh 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 c601ba630f..c53e314f3a 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 @@ -740,4 +740,30 @@ do end ) end +test.register_coroutine_test( + "Configuration:Report for OPERATING_MODE=1 should update to roller shutter profile", + function() + test.socket.zwave:__queue_receive({ + mock_fibaro_roller_shutter_venetian.id, + zw_test_utils.zwave_test_build_receive_command( + Configuration:Report({ parameter_number = 151, configuration_value = 1 }) + ) + }) + mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter" }) + end +) + +test.register_coroutine_test( + "Configuration:Report for OPERATING_MODE=2 should update to venetian profile", + function() + test.socket.zwave:__queue_receive({ + mock_fibaro_roller_shutter_venetian.id, + zw_test_utils.zwave_test_build_receive_command( + Configuration:Report({ parameter_number = 151, configuration_value = 2 }) + ) + }) + mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter-venetian" }) + end +) + test.run_registered_tests() 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 edaffb4214..d9bd11e01f 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 @@ -510,122 +510,113 @@ test.register_coroutine_test( end ) -do - test.register_coroutine_test( - "Mode should be changed to venetian blinds after receiving configuration report with value 1", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Configuration:Report({ - parameter_number = 71, - size = 1, - configuration_value = 1 - }) +test.register_coroutine_test( + "Mode should be changed to venetian blinds after receiving configuration report with value 1", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Configuration:Report({ + parameter_number = 71, + size = 1, + configuration_value = 1 }) - mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter-venetian" }) - end - ) -end + }) + mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter-venetian" }) + end +) -do - test.register_coroutine_test( - "Mode should be changed to shutter after receiving configuration report with value 0", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Configuration:Report({ - parameter_number = 71, - size = 1, - configuration_value = 0 - }) +test.register_coroutine_test( + "Mode should be changed to shutter after receiving configuration report with value 0", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Configuration:Report({ + parameter_number = 71, + size = 1, + configuration_value = 0 }) - mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter" }) - end - ) -end + }) + mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter" }) + end +) -do - test.register_coroutine_test( - "SwitchMultilevel:Set() should be correctly interpreted by the driver", - function() - local targetValue = 50 - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - SwitchMultilevel:Set({ - value = targetValue - }) +test.register_coroutine_test( + "SwitchMultilevel:Set() should be correctly interpreted by the driver", + function() + local targetValue = 50 + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + SwitchMultilevel:Set({ + value = targetValue }) - local expectedCachedEvent = utils.stringify_table(capabilities.windowShade.windowShade.opening()) - test.wait_for_events() - local actualCachedEvent = utils.stringify_table(mock_qubino_flush_shutter.transient_store.blinds_last_command) - assert(expectedCachedEvent == actualCachedEvent, "driver should cache 'opening' event when targetLevel > currentLevel") - assert(targetValue == mock_qubino_flush_shutter.transient_store.shade_target, "driver should chache correct level value") - end - ) -end + }) + local expectedCachedEvent = utils.stringify_table(capabilities.windowShade.windowShade.opening()) + test.wait_for_events() + local actualCachedEvent = utils.stringify_table(mock_qubino_flush_shutter.transient_store.blinds_last_command) + assert(expectedCachedEvent == actualCachedEvent, "driver should cache 'opening' event when targetLevel > currentLevel") + assert(targetValue == mock_qubino_flush_shutter.transient_store.shade_target, "driver should chache correct level value") + end +) -do - test.register_coroutine_test( - "Meter:Report() with meter_value > 0 should be correctly interpreted by the driver", - function() - local cachedShadesEvent = capabilities.windowShade.windowShade.opening() - local targetValue = 50 - mock_qubino_flush_shutter:set_field("blinds_last_command", cachedShadesEvent) - mock_qubino_flush_shutter:set_field("shade_target", targetValue) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 10 - }) +test.register_coroutine_test( + "Meter:Report() with meter_value > 0 should be correctly interpreted by the driver", + function() + local cachedShadesEvent = capabilities.windowShade.windowShade.opening() + local targetValue = 50 + mock_qubino_flush_shutter:set_field("blinds_last_command", cachedShadesEvent) + mock_qubino_flush_shutter:set_field("shade_target", targetValue) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Meter:Report({ + scale = Meter.scale.electric_meter.WATTS, + meter_value = 10 }) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", cachedShadesEvent) - ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(targetValue)) - ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 10, unit = "W"})) - ) - end - ) -end + }) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", cachedShadesEvent) + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(targetValue)) + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 10, unit = "W"})) + ) + end +) -do - test.register_coroutine_test( - "Meter:Report() with meter_value == 0 should be correctly interpreted by the driver", - function() - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_qubino_flush_shutter.id, - Meter:Report({ - scale = Meter.scale.electric_meter.WATTS, - meter_value = 0 - }) + +test.register_coroutine_test( + "Meter:Report() with meter_value == 0 should be correctly interpreted by the driver", + function() + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter.id, + Meter:Report({ + scale = Meter.scale.electric_meter.WATTS, + meter_value = 0 }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_qubino_flush_shutter, - SwitchMultilevel:Get({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_qubino_flush_shutter, - Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) - ) + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter, + SwitchMultilevel:Get({}) ) - test.socket.capability:__expect_send( - mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 0, unit = "W"})) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter, + Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}) ) - end - ) -end + ) + test.socket.capability:__expect_send( + mock_qubino_flush_shutter:generate_test_message("main", capabilities.powerMeter.power({value = 0, unit = "W"})) + ) + end +) test.register_message_test( "Energy meter reports should be generating events", @@ -651,4 +642,58 @@ test.register_message_test( } ) +test.register_coroutine_test( + "SwitchMultilevel:Set with lower target than current should cache closing command and fire Meter:Get after 4s", + function() + -- Pre-set state so currentLevel (80) > targetLevel (10) + mock_qubino_flush_shutter_venetian:update_state_cache_entry( + "main", capabilities.windowShadeLevel.ID, "shadeLevel", { value = 80 } + ) + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.zwave:__queue_receive({ + mock_qubino_flush_shutter_venetian.id, + SwitchMultilevel:Set({ value = 10 }) + }) + test.wait_for_events() + test.mock_time.advance_time(4) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Meter:Get({ scale = Meter.scale.electric_meter.WATTS }) + ) + ) + end +) + +do + local new_param_value = 1 + test.register_coroutine_test( + "infoChanged on venetian qubino should send Configuration:Set then Configuration:Get after 1s", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + local device_data = utils.deep_copy(mock_qubino_flush_shutter_venetian.raw_st_data) + device_data.preferences["operatingModes"] = new_param_value + local device_data_json = dkjson.encode(device_data) + test.socket.device_lifecycle:__queue_receive({ mock_qubino_flush_shutter_venetian.id, "infoChanged", device_data_json }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Configuration:Set({ + parameter_number = 71, + configuration_value = new_param_value, + size = 1 + }) + ) + ) + test.mock_time.advance_time(1) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_qubino_flush_shutter_venetian, + Configuration:Get({ parameter_number = 71 }) + ) + ) + end + ) +end + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua index e2134b1163..8913a42e53 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_iblinds_window_treatment.lua @@ -475,4 +475,52 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting window shade level to 0 on iblinds v1 should emit windowShade.closed", + function() + test.socket.capability:__queue_receive( + { + mock_blind.id, + { capability = "windowShadeLevel", command = "setShadeLevel", args = { 0 } } + } + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_blind:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_blind, + SwitchMultilevel:Set({ value = 0 }) + ) + ) + end +) + +test.register_coroutine_test( + "Setting window shade level to 0 on iblinds v3 should emit windowShade.closed", + function() + test.socket.capability:__queue_receive( + { + mock_blind_v3.id, + { capability = "windowShadeLevel", command = "setShadeLevel", args = { 0 } } + } + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_blind_v3:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_blind_v3, + SwitchMultilevel:Set({ value = 0 }) + ) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua index fa6fd809e7..be25647509 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_window_treatment.lua @@ -531,4 +531,43 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting window shade preset on basic-only device should generate Basic:Set and Basic:Get", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.capability:__queue_receive( + { + mock_window_shade_basic.id, + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + } + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_window_shade_basic, + Basic:Set({ value = 50 }) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(5) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_window_shade_basic, + Basic:Get({}) + ) + ) + end +) + +test.register_coroutine_test( + "Adding a window treatment device should emit supportedWindowShadeCommands", + function() + test.socket.device_lifecycle():__queue_receive({ mock_window_shade_basic.id, "added" }) + test.socket.capability:__expect_send( + mock_window_shade_basic:generate_test_message("main", capabilities.windowShade.supportedWindowShadeCommands( + {"open", "close", "pause"}, { visibility = { displayed = false } } + )) + ) + end +) + test.run_registered_tests() From fb5a39ee38be37b219bea92eefbfe5d4a95517d8 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 24 Feb 2026 13:59:25 -0800 Subject: [PATCH 441/449] WWSTCERT-10480 GELUBU Door Lock G30 --- drivers/SmartThings/zigbee-lock/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-lock/fingerprints.yml b/drivers/SmartThings/zigbee-lock/fingerprints.yml index 5c04d78ce5..083dd3a11e 100644 --- a/drivers/SmartThings/zigbee-lock/fingerprints.yml +++ b/drivers/SmartThings/zigbee-lock/fingerprints.yml @@ -5,6 +5,11 @@ zigbeeManufacturer: manufacturer: GELUBU model: S93 deviceProfileName: lock-battery + - id: "GELUBU/G30" + deviceLabel: GELUBU Door Lock G30 + manufacturer: GELUBU + model: G30 + deviceProfileName: lock-battery # YALE - id: "Yale YRD220/240" deviceLabel: "Yale Door Lock" From 3087d96ea7ee75d45009f08570736a0947dcbe59 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 25 Feb 2026 11:12:12 -0600 Subject: [PATCH 442/449] revert the recent profile components check additions --- .../matter-lock/src/new-matter-lock/init.lua | 33 ++------ .../air_quality_sensor_utils/utils.lua | 24 ++---- .../sub_drivers/air_quality_sensor/init.lua | 3 +- .../SmartThings/matter-switch/src/init.lua | 3 +- .../sub_drivers/camera/camera_utils/utils.lua | 18 +++++ .../src/sub_drivers/camera/init.lua | 2 +- .../src/switch_utils/device_configuration.lua | 1 + .../matter-switch/src/switch_utils/fields.lua | 2 + .../matter-switch/src/switch_utils/utils.lua | 27 ------- .../src/test/test_matter_light_fan.lua | 81 ++----------------- 10 files changed, 45 insertions(+), 149 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index a91790cedb..11aa2b0884 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -22,6 +22,8 @@ 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 MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED" + local RESPONSE_STATUS_MAP = { [DoorLock.types.DlStatus.SUCCESS] = "success", [DoorLock.types.DlStatus.FAILURE] = "failure", @@ -201,6 +203,7 @@ local function match_profile_modular(driver, device) table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) + device:set_field(MODULAR_PROFILE_UPDATED, true) end local function match_profile_switch(driver, device) @@ -238,37 +241,11 @@ local function match_profile_switch(driver, device) device:try_update_metadata({profile = profile_name}) end -local function profile_changed(latest_profile, previous_profile) - if latest_profile.id ~= previous_profile.id then - return true - end - for component_id, synced_component in pairs(latest_profile.components or {}) do - local prev_component = previous_profile.components[component_id] - if prev_component == nil then - return true - end - if #synced_component.capabilities ~= #prev_component.capabilities then - return true - end - -- Build a table of capability IDs from the previous component. Then, use this map to check - -- that all capabilities in the synced component existed in the previous component. - local prev_cap_ids = {} - for _, capability in ipairs(prev_component.capabilities or {}) do - prev_cap_ids[capability.id] = true - end - for _, capability in ipairs(synced_component.capabilities or {}) do - if not prev_cap_ids[capability.id] then - return true - end - end - end - return false -end - local function info_changed(driver, device, event, args) - if not profile_changed(device.profile, args.old_st_store.profile) then + if device.profile.id == args.old_st_store.profile.id and not device:get_field(MODULAR_PROFILE_UPDATED) then return end + device:set_field(MODULAR_PROFILE_UPDATED, nil) for cap_id, attributes in pairs(subscribed_attributes) do if device:supports_capability_by_id(cap_id) then for _, attr in ipairs(attributes) do 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 5c1726d6d8..95ca80964c 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 @@ -77,26 +77,17 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device) end end -function AirQualitySensorUtils.profile_changed(latest_profile, previous_profile) - if latest_profile.id ~= previous_profile.id then +function AirQualitySensorUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then return true end - for component_id, synced_component in pairs(latest_profile.components or {}) do - local prev_component = previous_profile.components[component_id] - if prev_component == nil then + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then return true end - if #synced_component.capabilities ~= #prev_component.capabilities then - return true - end - -- Build a table of capability IDs from the previous component. Then, use this map to check - -- that all capabilities in the synced component existed in the previous component. - local prev_cap_ids = {} - for _, capability in ipairs(prev_component.capabilities or {}) do - prev_cap_ids[capability.id] = true - end - for _, capability in ipairs(synced_component.capabilities or {}) do - if not prev_cap_ids[capability.id] then + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then return true end end @@ -104,5 +95,4 @@ function AirQualitySensorUtils.profile_changed(latest_profile, previous_profile) return false 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 41c6514b40..98b8430c98 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 @@ -66,7 +66,8 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) end function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) - if aqs_utils.profile_changed(device.profile, args.old_st_store.profile) then + if device.profile.id ~= args.old_st_store.profile.id or + aqs_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) 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-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 12ee2b3662..cac42e4483 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -64,7 +64,8 @@ function SwitchLifecycleHandlers.driver_switched(driver, device) end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) - if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then + if device.profile.id ~= args.old_st_store.profile.id or device:get_field(fields.MODULAR_PROFILE_UPDATED) then + device:set_field(fields.MODULAR_PROFILE_UPDATED, nil) if device.network_type == device_lib.NETWORK_TYPE_MATTER then device:subscribe() button_cfg.configure_buttons(device, diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua index 5f3205d73b..1caa9737bb 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/utils.lua @@ -134,6 +134,24 @@ function CameraUtils.build_supported_resolutions(device, max_encoded_pixel_rate, return resolutions end +function CameraUtils.profile_changed(synced_components, prev_components) + if #synced_components ~= #prev_components then + return true + end + for _, component in pairs(synced_components or {}) do + if (prev_components[component.id] == nil) or + (#component.capabilities ~= #prev_components[component.id].capabilities) then + return true + end + for _, capability in pairs(component.capabilities or {}) do + if prev_components[component.id][capability.id] == nil then + return true + end + end + end + return false +end + function CameraUtils.optional_capabilities_list_changed(new_component_capability_list, previous_component_capability_list) local previous_capability_map = {} local component_sizes = {} diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index c32e131572..f13589ff41 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -47,7 +47,7 @@ function CameraLifecycleHandlers.driver_switched(driver, device) end function CameraLifecycleHandlers.info_changed(driver, device, event, args) - if switch_utils.profile_changed(device.profile, args.old_st_store.profile) then + if camera_utils.profile_changed(device.profile.components, args.old_st_store.profile.components) then camera_cfg.initialize_camera_capabilities(device) device:subscribe() if #switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.DOORBELL) > 0 then 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 98a300924f..8542972320 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -238,6 +238,7 @@ function DeviceConfiguration.match_profile(driver, device) local fan_device_type_ep_ids = switch_utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.FAN) if #fan_device_type_ep_ids > 0 then updated_profile, optional_component_capabilities = FanDeviceConfiguration.assign_profile_for_fan_ep(device, default_endpoint_id) + device:set_field(fields.MODULAR_PROFILE_UPDATED, true) end -- initialize the main device card with buttons if applicable diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index ec620eaa65..6eb03b1472 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -147,6 +147,8 @@ SwitchFields.ELECTRICAL_SENSOR_EPS = "__electrical_sensor_eps" --- for an Electrical Sensor EP with a "primary" endpoint, used during device profiling. SwitchFields.ELECTRICAL_TAGS = "__electrical_tags" +SwitchFields.MODULAR_PROFILE_UPDATED = "__modular_profile_updated" + SwitchFields.profiling_data = { POWER_TOPOLOGY = "__power_topology", BATTERY_SUPPORT = "__battery_support", diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index 16d602cfc1..0592d9a342 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -325,33 +325,6 @@ function utils.create_multi_press_values_list(size, supportsHeld) return list end -function utils.profile_changed(latest_profile, previous_profile) - if latest_profile.id ~= previous_profile.id then - return true - end - for component_id, synced_component in pairs(latest_profile.components or {}) do - local prev_component = previous_profile.components[component_id] - if prev_component == nil then - return true - end - if #synced_component.capabilities ~= #prev_component.capabilities then - return true - end - -- Build a table of capability IDs from the previous component. Then, use this map to check - -- that all capabilities in the synced component existed in the previous component. - local prev_cap_ids = {} - for _, capability in ipairs(prev_component.capabilities or {}) do - prev_cap_ids[capability.id] = true - end - for _, capability in ipairs(synced_component.capabilities or {}) do - if not prev_cap_ids[capability.id] then - return true - end - end - end - return false -end - function utils.detect_bridge(device) return #utils.get_endpoints_by_device_type(device, fields.DEVICE_TYPE_ID.AGGREGATOR) > 0 end 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 b5ae89fd7a..e1af3ad52b 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 @@ -16,8 +16,7 @@ local mock_device_ep2 = 2 local mock_device = test.mock_device.build_test_matter_device({ label = "Matter Fan Light", - profile = t_utils.get_profile_definition("fan-modular.yml", - {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}}), + profile = t_utils.get_profile_definition("fan-modular.yml", {}), manufacturer_info = { vendor_id = 0x0000, product_id = 0x0000, @@ -59,40 +58,6 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) -local mock_device_capabilities_disabled = test.mock_device.build_test_matter_device({ - label = "Matter Fan Light", - profile = t_utils.get_profile_definition("fan-modular.yml", - {enabled_optional_capabilities = {{"main", {}}}}), - manufacturer_info = { - vendor_id = 0x0000, - product_id = 0x0000, - }, - matter_version = { - software = 1, - hardware = 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 = mock_device_ep2, - clusters = { - {cluster_id = clusters.FanControl.ID, cluster_type = "SERVER", feature_map = 15}, - }, - device_types = { - {device_type_id = 0x002B, device_type_revision = 1,} -- Fan - } - } - } -}) - local CLUSTER_SUBSCRIBE_LIST ={ clusters.OnOff.attributes.OnOff, clusters.LevelControl.attributes.CurrentLevel, @@ -145,48 +110,16 @@ local function test_init() }) mock_device:expect_metadata_update({ profile = "fan-modular", optional_component_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}} }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + + local updated_device_profile = t_utils.get_profile_definition("fan-modular.yml", + {enabled_optional_capabilities = {{"main", {"fanSpeedPercent", "fanMode"}}}} + ) + 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 test.set_test_init_function(test_init) -test.register_coroutine_test( - "Component-capability update without profile ID update should cause re-subscribe in infoChanged handler", function() - local cluster_subscribe_list ={ - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.PercentCurrent, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled) - for i, clus in ipairs(cluster_subscribe_list) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end - end - test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed( - {profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={{id="fanSpeedPercent", version=1}, {id="fanMode", version=1}, {id="firmwareUpdate", version=1}, {id="refresh", version=1}}}}}}) - ) - 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.register_coroutine_test( - "No component-capability update an no profile ID update should not cause a re-subscribe in infoChanged handler", function() - local cluster_subscribe_list ={ - clusters.FanControl.attributes.FanModeSequence, - clusters.FanControl.attributes.FanMode, - clusters.FanControl.attributes.PercentCurrent, - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_capabilities_disabled) - for i, clus in ipairs(cluster_subscribe_list) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_capabilities_disabled)) end - end - test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed( - {profile = {id = "00000000-1111-2222-3333-000000000004", components = { main = {capabilities={{id="firmwareUpdate", version=1}, {id="refresh", version=1}}}}}}) - ) - end, - { test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end } -) - - test.register_coroutine_test( "Switch capability should send the appropriate commands", function() test.socket.capability:__queue_receive( From 48451c814f036d0c718d0ab865ed21e5e299d7af Mon Sep 17 00:00:00 2001 From: cjswedes Date: Tue, 24 Feb 2026 15:53:44 -0600 Subject: [PATCH 443/449] Improve Zigbee driver test coverage --- .../test_MultiIR_air_quality_detector.lua | 194 ++++++++++++++++++ .../src/test/test_shus_mattress.lua | 37 ++++ .../zigbee-button/src/aqara/init.lua | 1 + .../src/test/test_aqara_button.lua | 35 ++++ .../src/test/test_frient_contact_sensor.lua | 62 +++--- .../src/test/test_smartsense_multi.lua | 67 ++++++ .../test/test_smartthings_multi_sensor.lua | 32 +++ .../src/test/test_zigbee_accessory_dimmer.lua | 77 +++++++ .../test_zigbee_battery_accessory_dimmer.lua | Bin 22479 -> 28081 bytes .../zigbee-fan/src/test/test_fan_light.lua | 42 ++++ .../src/test/test_aqara_sensor.lua | 25 +++ .../test/test_frient_air_quality_sensor.lua | 35 ++++ .../src/test/test_frient_sensor.lua | 33 +++ .../test/test_illuminance_sensor_aqara.lua | 52 +++++ .../zigbee-lock/src/test/test_zigbee_lock.lua | 84 ++++++++ .../zigbee-lock/src/test/test_zigbee_yale.lua | 94 +++++++++ .../test/test_aqara_motion_illuminance.lua | 82 ++++++++ .../src/test/test_frient_motion_sensor.lua | 77 ++++--- .../test/test_frient_motion_sensor_pro.lua | 79 +++---- .../src/test/test_gator_motion.lua | 16 ++ .../src/test/test_ikea_motion.lua | 25 +++ .../src/test/test_thirdreality_sensor.lua | 19 ++ .../src/test/test_zigbee_power_meter_1p.lua | 96 +++++++++ ...e_power_meter_consumption_report_sihas.lua | 36 ++++ .../test/test_zigbee_power_meter_frient.lua | 102 +++++++++ .../src/test/test_zigbee_presence_sensor.lua | 93 +++++++++ .../test_frient_zigbee_range_extender.lua | 32 +++ .../src/test/test_frient_siren.lua | 116 +++++++++++ .../src/test/test_frient_siren_tamper.lua | 54 +++++ .../src/test/test_zigbee_siren.lua | 57 +++++ .../src/test/test_aqara_gas_detector.lua | 60 ++++++ .../src/test/test_aqara_smoke_detector.lua | 62 ++++-- .../src/test/test_frient_heat_detector.lua | 32 +++ .../src/test/test_frient_smoke_detector.lua | 90 ++++++++ .../src/test/test_aqara_light.lua | 34 +++ .../src/test/test_ge_link_bulb.lua | 34 +++ .../src/test/test_jasco_switch.lua | 31 +++ .../src/test/test_laisiao_bath_heather.lua | 17 ++ .../src/test/test_multi_switch_no_master.lua | 41 ++++ .../src/test/test_wallhero_switch.lua | 32 ++- .../src/test/test_sinope_thermostat.lua | 27 +++ .../test_stelpro_ki_zigbee_thermostat.lua | 46 +++++ .../src/test/test_zenwithin_thermostat.lua | 40 ++++ .../zigbee-valve/src/test/test_ezex_valve.lua | 16 ++ .../src/test/test_sinope_valve.lua | 8 + .../src/test/test_leaksmart_water.lua | 30 +-- .../test_thirdreality_water_leak_sensor.lua | 8 + .../test/test_thirdreality_watering_kit.lua | 46 +++++ .../test_zigbee_window_shade_battery_ikea.lua | 110 ++++++++++ ...est_zigbee_window_shade_battery_yoolax.lua | 105 ++++++++++ .../test_zigbee_window_treatment_somfy.lua | 92 +++++++++ .../test_zigbee_window_treatment_vimar.lua | 118 +++++++++++ 52 files changed, 2684 insertions(+), 149 deletions(-) create mode 100644 drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua 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 883a3dc5a5..09f539b53b 100755 --- 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 @@ -227,4 +227,198 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "AQI moderate (51-100) emits moderate airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 75 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "AQI slightlyUnhealthy (101-150) emits slightlyUnhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 125 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "slightlyUnhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI unhealthy (151-200) emits unhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 175 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI veryUnhealthy (201-300) emits veryUnhealthy airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 250 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "veryUnhealthy"}))) + end +) + +test.register_coroutine_test( + "AQI hazardous (>=301) emits hazardous airQualityHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 350 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC5, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "hazardous"}))) + end +) + +test.register_coroutine_test( + "carbonDioxide moderate (1501-2500) emits moderate carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 2000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 2000, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "carbonDioxide unhealthy (>2500) emits unhealthy carbonDioxideHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 3000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC3, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideMeasurement.carbonDioxide({value = 3000, unit = "ppm"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm2.5 moderate emits moderate fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 90 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 90}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "moderate"}))) + end +) + +test.register_coroutine_test( + "pm2.5 unhealthy (>=115) emits unhealthy fineDustHealthConcern", + function() + local attr_report_data = { + { 0x0000, data_types.Uint16.ID, 120 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustSensor.fineDustLevel({value = 120}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm1.0 unhealthy (>100) emits unhealthy veryFineDustHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.Uint16.ID, 150 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustSensor.veryFineDustLevel({value = 150}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "pm10 unhealthy (>150) emits unhealthy dustHealthConcern", + function() + local attr_report_data = { + { 0x0002, data_types.Uint16.ID, 200 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC1, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustSensor.dustLevel({value = 200}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.dustHealthConcern.dustHealthConcern({value = "unhealthy"}))) + end +) + +test.register_coroutine_test( + "tvoc good (<600) emits good tvocHealthConcern", + function() + local attr_report_data = { + { 0x0001, data_types.SinglePrecisionFloat.ID, SinglePrecisionFloat(0, 8, 0.953125) } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, 0xFCC2, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({value = 500.0, unit = "ug/m3"}))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({value = "good"}))) + end +) + test.run_registered_tests() 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 7e5ade0cf5..a3227b04bc 100755 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -398,6 +398,21 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Device reported yoga 3 and driver emit custom_capabilities.yoga.state.both()", + function() + local attr_report_data = { + { 0x0008, data_types.Uint8.ID, 3 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.yoga.state.both())) + end +) + test.register_coroutine_test( "Device reported yoga 2 and driver emit custom_capabilities.yoga.state.right()", function() @@ -910,4 +925,26 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "capability left_control backControl soft emits idle event after delay", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = custom_capabilities.left_control.ID, component = "main", command ="backControl" , args = {"soft"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.left_control.leftback.soft())) + test.wait_for_events() + + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + custom_capabilities.left_control.leftback("idle", { visibility = { displayed = false }}))) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-button/src/aqara/init.lua b/drivers/SmartThings/zigbee-button/src/aqara/init.lua index 7467fcfe42..b9545e4721 100644 --- a/drivers/SmartThings/zigbee-button/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-button/src/aqara/init.lua @@ -85,6 +85,7 @@ local function battery_level_handler(driver, device, value, zb_rx) batteryLevel = "warning" end + -- Note that all aqara buttons use batteryLevel and not battery capability. if device:supports_capability_by_id(capabilities.battery.ID) then device:emit_event(capabilities.battery.battery(calc_battery_percentage(voltage))) elseif device:supports_capability_by_id(capabilities.batteryLevel.ID) then 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 67314ef651..b2d5a986d6 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -21,6 +21,21 @@ local PRIVATE_ATTRIBUTE_ID_ALIVE = 0x00F7 local MODE_CHANGE = "stse.allowOperationModeChange" local COMP_LIST = { "button1", "button2", "all" } + +local mock_device_h1_single = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("aqara-single-button-mode.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.remote.b18ac1", + server_clusters = { 0x0001, 0x0012 } + } + } + } +) + local mock_device_e1 = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("one-button-batteryLevel.yml"), @@ -51,6 +66,7 @@ local mock_device_h1_double_rocker = 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_h1_single) test.mock_device.add_test_device(mock_device_e1) test.mock_device.add_test_device(mock_device_h1_double_rocker) end @@ -286,4 +302,23 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle added lifecycle - H1 single rocker (sets mode=1)", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_h1_single.id, "added" }) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.battery.normal())) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.type("CR2450"))) + test.socket.capability:__expect_send(mock_device_h1_single:generate_test_message("main", + capabilities.batteryLevel.quantity(1))) + end +) + test.run_registered_tests() 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 bd7e7512dd..b73f7ee3fe 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 @@ -165,36 +165,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() - --- test.mock_time.advance_time(50000) -- battery is 21600 for max reporting interval --- test.socket.zigbee:__set_channel_ordering("relaxed") - --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device) --- } --- ) - --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_message_test( "Refresh should read all necessary attributes", { @@ -260,4 +230,36 @@ test.register_message_test( } ) +test.register_message_test( + "ZoneStatusChangeNotification should be handled: contact/open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: contact/closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + test.run_registered_tests() 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 035c49d27f..d5482e7d6e 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua @@ -5,6 +5,9 @@ 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 IASZone = clusters.IASZone local SMARTSENSE_PROFILE_ID = 0xFC01 local MFG_CODE = 0x110A @@ -426,4 +429,68 @@ test.register_coroutine_test( end ) +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + +test.register_message_test( + "ZoneStatus attr report should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatus attr report should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + test.run_registered_tests() 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 5ef40d5aaf..c86078f224 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 @@ -219,6 +219,38 @@ test.register_coroutine_test( end ) +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should generate contact event when garageSensor not set: closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + } + } +) + test.register_coroutine_test( "Refresh necessary attributes", function() diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua index 90d29656d8..751fc1bee9 100644 --- a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua +++ b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_accessory_dimmer.lua @@ -217,4 +217,81 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "On command when current level is 0 should reset level then toggle switch", + function() + mock_device:set_field("current_level", 0) + test.socket.zigbee:__queue_receive({ mock_device.id, OnOff.server.commands.On.build_test_rx(mock_device) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(10))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) + +test.register_coroutine_test( + "Step command(MoveStepMode.DOWN) to level 0 should emit switch off and level 0", + function() + mock_device:set_field("current_level", 10) + local step_command = Level.server.commands.Step.build_test_rx(mock_device, Level.types.MoveStepMode.DOWN, 0x00, + 0x0000, 0x00, 0x00) + local frm_ctrl = FrameCtrl(0x01) + step_command.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({ mock_device.id, step_command }) + 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("main", capabilities.switchLevel.level(0))) + end +) + +test.register_coroutine_test( + "Step command(MoveStepMode.UP) when device is off should emit switch on", + function() + mock_device:set_field("current_status", "off") + local step_command = Level.server.commands.Step.build_test_rx(mock_device, Level.types.MoveStepMode.UP, 0x00, + 0x0000, 0x00, 0x00) + local frm_ctrl = FrameCtrl(0x01) + step_command.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({ mock_device.id, step_command }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(100))) + end +) + +test.register_coroutine_test( + "Capability command setLevel should be handled", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 50 } } }) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(50))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Capability command setLevel 0 should restore previous level", + function() + mock_device:set_field("current_level", 30) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 0 } } }) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(30))) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "device added lifecycle should initialize device state", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(100))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua b/drivers/SmartThings/zigbee-dimmer-remote/src/test/test_zigbee_battery_accessory_dimmer.lua index b16fe8cd11536c8bf2e155496b8b72e4c2497dc6..243f55946688f45d6e8bfe18217e6f2cf025daf2 100644 GIT binary patch delta 1013 zcmb7DOKTHR6eh)rEp5}#nh4tFOb8Oem>CcX9SyAu)7Kj1>$xYIk6OkY94)eQHZ^PTfO?)z7sH=jJOx2qTo z9mBWjvBk1pR#i~6rp1T}WgQfw(UA2Ttk;PS8fg&?s0AWeb)%`(fHWxU9-G7>4D4EK zoie2khK|12xD<`TGQa5bGK2TFs0_Sz!9i2xHF=e4lx?`P5Mm`96DDwe(u?0N4x<4> z_%gPFUg0j@6Mvlp;FycXItSs?IxFvAm9lzwoXkK(F`sZCzK;g*;c%G4n79@g>XSqq z%_h++CGmdJho>(@;x*Eu3IVxRBQ?;dN|X&nBQ9C0Q$}Tt-XYK}w3y7$mpP7Kq<%al zjN^|hVGIaA@o9Ph{n-%=mtywpfsg|kf;Qd=2C*eYa3_5vRWTe5s(j%N3rD0G@wRwS z;9WsMHB2bY7cM`^wfjU7H&b)?IW@ZVG55lQ-b;gcBKy4ZSJYy$W24eEO`^M&{L{|8 zc_#zCMyk}b*de?8Xii?k-?4tKa}sx^)7-L6{!Q4LV?_#`KWN`oqeXf)<^k$j$i*lY zQOLx%=5o(Gd%hVcPQ`E{yNWx$5qwlQyOiUUOzsXNU}L&zf_%BUcqqO+7Wwk^>_Xw@ za;323+UsQ5m0DHp@MaEk-rp9H#54b$<;LJ3HYQh&N+Ps*8f=a1qg#m?oSu5@{C`c% wI@yd1xn8L{bar3mu1>%?am?r}v}CPGz$CI#C;X*4+I@(>^WcMmAGfRiKct;m9{>OV delta 13 VcmdmZoALa5#tl2eC(q6m0{}6E27Ukl 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 0fa987e35a..1a61602c1e 100644 --- a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua +++ b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua @@ -432,4 +432,46 @@ test.register_message_test( } ) +test.register_message_test( + "Fan switch on command from main component", + { + { + channel = "capability", + direction = "receive", + message = { mock_base_device.id, { capability = "switch", component = "main", command = "on", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:write(mock_base_device, 1) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:read(mock_base_device) } + } + } +) + +test.register_message_test( + "Fan switch off command from main component", + { + { + channel = "capability", + direction = "receive", + message = { mock_base_device.id, { capability = "switch", component = "main", command = "off", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:write(mock_base_device, 0x00) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_base_device.id, FanControl.attributes.FanMode:read(mock_base_device) } + } + } +) + test.run_registered_tests() 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 497395ac55..5ced35a0d3 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 @@ -18,11 +18,17 @@ 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 cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" local PowerConfiguration = clusters.PowerConfiguration local TemperatureMeasurement = clusters.TemperatureMeasurement local RelativeHumidity = clusters.RelativeHumidity +local PRIVATE_CLUSTER_ID = 0xFCC0 +local PRIVATE_ATTRIBUTE_ID = 0x0009 +local MFG_CODE = 0x115F + local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("humidity-temp-battery-aqara.yml"), @@ -256,4 +262,23 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Added handler should send manufacturer attribute and initialize device state", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + 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.temperatureMeasurement.temperature({ value = 0, unit = "C" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.relativeHumidityMeasurement.humidity(0))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) + test.wait_for_events() + end +) + test.run_registered_tests() 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 e985dafce5..063afbdf9f 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 @@ -123,6 +123,7 @@ test.register_message_test( test.register_coroutine_test( "Configure should configure all necessary attributes", function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") @@ -191,6 +192,26 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + --refresh happens after configure + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, Frient_VOCMeasurement.ID, Frient_VOCMeasurement.attributes.MeasuredValue.ID, Frient_VOCMeasurement.ManufacturerSpecificCode):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(0x26) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) end ) @@ -291,4 +312,18 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Added handler should initialize VOC and air quality state", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.airQualitySensor.airQuality({ value = 0 }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocHealthConcern.tvocHealthConcern({ value = "good" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.tvocMeasurement.tvocLevel({ value = 0, unit = "ppb" }))) + test.wait_for_events() + end +) + test.run_registered_tests() 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 5b9ef77634..7d925ae94d 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 @@ -105,6 +105,7 @@ test.register_message_test( test.register_coroutine_test( "Configure should configure all necessary attributes", function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.zigbee:__expect_send({ @@ -141,6 +142,22 @@ test.register_coroutine_test( TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(mock_device, 0x001E, 0x0E10, 100) }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.wait_for_events() end ) @@ -190,6 +207,7 @@ test.register_message_test( test.register_coroutine_test( "info_changed to check for necessary preferences settings: Temperature Sensitivity", function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") local updates = { preferences = { temperatureSensitivity = 0.9, @@ -217,6 +235,21 @@ test.register_coroutine_test( ) }) test.wait_for_events() + + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + HumidityMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) + }) + test.wait_for_events() end ) 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 557d4e2345..81fee9249f 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 @@ -9,6 +9,10 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local messages = require "st.zigbee.messages" +local zb_const = require "st.zigbee.constants" +local write_attribute_response = require "st.zigbee.zcl.global_commands.write_attribute_response" +local zcl_messages = require "st.zigbee.zcl" test.add_package_capability("detectionFrequency.yaml") local IlluminanceMeasurement = clusters.IlluminanceMeasurement @@ -19,8 +23,10 @@ local detectionFrequency = capabilities["stse.detectionFrequency"] local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F +local FREQUENCY_ATTRIBUTE_ID = 0x0000 local FREQUENCY_DEFAULT_VALUE = 5 +local FREQUENCY_PREF = "frequencyPref" local mock_device = test.mock_device.build_test_zigbee_device( { @@ -131,4 +137,50 @@ test.register_message_test( } ) +local function build_write_attr_res(cluster, status) + local addr_header = messages.AddressHeader( + mock_device:get_short_address(), + mock_device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local write_attribute_body = write_attribute_response.WriteAttributeResponse(status, {}) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(write_attribute_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = write_attribute_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_coroutine_test( + "Handle setDetectionFrequency capability command", + function() + local frequency = 10 + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "stse.detectionFrequency", component = "main", command = "setDetectionFrequency", args = { frequency } } }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, FREQUENCY_ATTRIBUTE_ID, + MFG_CODE, data_types.Uint16, frequency) }) + end +) + +test.register_coroutine_test( + "Handle write attr res on PRIVATE_CLUSTER_ID emits detectionFrequency", + function() + mock_device:set_field(FREQUENCY_PREF, FREQUENCY_DEFAULT_VALUE) + test.socket.zigbee:__queue_receive({ mock_device.id, + build_write_attr_res(PRIVATE_CLUSTER_ID, 0x00) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + detectionFrequency.detectionFrequency(FREQUENCY_DEFAULT_VALUE, { visibility = { displayed = false } }))) + end +) + 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.lua index 3ed037cd54..2fd55fd3f0 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua @@ -771,4 +771,88 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Alarm code 0 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x00, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + } +) + +test.register_message_test( + "Alarm code 1 should generate lock unknown event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Alarm.client.commands.Alarm.build_test_rx(mock_device, 0x01, DoorLock.ID) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + } + } +) + +test.register_message_test( + "Pin response for unoccupied slot with no existing code should generate unset event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x05, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("5 unset", + { data = { codeName = "Code 5" }, state_change = true })) + } + } +) + +test.register_coroutine_test( + "Pin response for already-set slot should use changed change type", + 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() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 0x01, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "Code 1" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) + end +) + 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.lua index 75ad49a1f5..931a4b143c 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua @@ -319,4 +319,98 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Setting a user code for a slot that is empty should indicate failure and unset", + function() + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + 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.mock_time.advance_time(4) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.GetPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 is not set", { state_change = true }))) + end +) + +test.register_coroutine_test( + "Pin response for already-set slot without pending operation should use changed change type", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 changed", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + end +) + +test.register_coroutine_test( + "Pin response for already-set slot that is now empty should delete the code", + function() + init_code_slot(1, "initialName", mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) + test.wait_for_events() + + test.socket.zigbee:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, + DoorLockUserStatus.OCCUPIED_DISABLED, + DoorLockUserType.UNRESTRICTED, + "" + ) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "initialName" }, state_change = true }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false }}))) + end +) + test.run_registered_tests() 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 2580af0ceb..c8bb2c20f0 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 @@ -10,6 +10,10 @@ local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local messages = require "st.zigbee.messages" +local zb_const = require "st.zigbee.constants" +local write_attribute_response = require "st.zigbee.zcl.global_commands.write_attribute_response" +local zcl_messages = require "st.zigbee.zcl" test.add_package_capability("sensitivityAdjustment.yaml") test.add_package_capability("detectionFrequency.yaml") @@ -46,6 +50,29 @@ local function test_init() test.set_test_init_function(test_init) +local function build_write_attr_res(cluster, status) + local addr_header = messages.AddressHeader( + mock_device:get_short_address(), + mock_device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local write_attribute_body = write_attribute_response.WriteAttributeResponse(status, {}) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(write_attribute_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = write_attribute_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + test.register_coroutine_test( "Handle added lifecycle", function() @@ -123,4 +150,59 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Motion detected twice cancels existing timer and creates a new one", + function() + local detect_duration = PREF_FREQUENCY_VALUE_DEFAULT + -- Pre-register two timers: first will be cancelled, second will fire + test.timer.__create_and_queue_test_time_advance_timer(detect_duration, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(detect_duration, "oneshot") + local attr_report_data = { + { MOTION_ILLUMINANCE_ATTRIBUTE_ID, data_types.Int32.ID, 0x0001006E } -- 65646 + } + -- First motion event + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(110)) + ) + test.wait_for_events() + -- Second motion event before first timer fires - cancels first timer + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(110)) + ) + test.wait_for_events() + -- Only the second timer fires + test.mock_time.advance_time(detect_duration) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.motionSensor.motion.inactive())) + end +) + +test.register_coroutine_test( + "WriteAttributeResponse with PREF_FREQUENCY_KEY updates detection frequency", + function() + mock_device:set_field(PREF_CHANGED_KEY, PREF_FREQUENCY_KEY) + mock_device:set_field(PREF_CHANGED_VALUE, PREF_FREQUENCY_VALUE_DEFAULT) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_write_attr_res(PRIVATE_CLUSTER_ID, 0x00) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + detectionFrequency.detectionFrequency(PREF_FREQUENCY_VALUE_DEFAULT, {visibility = {displayed = false}}))) + end +) + test.run_registered_tests() 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 89d014dc90..3e3acf69e2 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 @@ -72,39 +72,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() --- test.mock_time.advance_time(50000) --- test.socket.zigbee:__set_channel_ordering("relaxed") --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_coroutine_test( "Refresh should read all necessary attributes", function() @@ -130,6 +97,7 @@ test.register_coroutine_test( ) test.wait_for_events() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zigbee:__expect_send({ @@ -227,9 +195,52 @@ test.register_coroutine_test( }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + -- Advance time to trigger the do_refresh call scheduled in do_configure + test.wait_for_events() + test.mock_time.advance_time(5) + test.socket.zigbee:__expect_send({ + mock_device.id, + OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) + }) end ) +test.register_message_test( + "Occupancy attribute handler emits motion active", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_device, 0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + +test.register_message_test( + "Occupancy attribute handler emits motion inactive", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_device, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + test.register_coroutine_test( "infochanged to check for necessary preferences settings: Motion Turn-Off Delay, Motion Turn-On Delay, Movement Threshold in Turn-On Delay", function() 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 9556a6e58e..1f72d365e6 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 @@ -176,51 +176,6 @@ test.register_message_test( } ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.wait_for_events() --- test.mock_time.advance_time(50000) --- test.socket.zigbee:__set_channel_ordering("relaxed") --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IASZone.attributes.ZoneStatus:read(mock_device):to_endpoint(TAMPER_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_ENDPOINT) --- } --- ) --- test.socket.zigbee:__expect_send( --- { --- mock_device.id, --- TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_MEASUREMENT_ENDPOINT) --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) - test.register_coroutine_test( "Refresh should read all necessary attributes", function() @@ -432,4 +387,38 @@ test.register_coroutine_test( end ) +test.register_message_test( + "IASZone attribute status handler: tamper detected", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 1 + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0004) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.detected()) + } + } +) + +test.register_message_test( + "IASZone attribute status handler: tamper clear", + { + { + channel = "zigbee", + direction = "receive", + -- ZoneStatus | Bit2: Tamper set to 0 + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) + } + } +) + test.run_registered_tests() 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 7f47416e93..3afdbbcaa2 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 @@ -143,4 +143,20 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "ZoneStatusChangeNotification with alarm1 triggers motion active and inactive", + function() + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) + } + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.mock_time.advance_time(120) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + test.run_registered_tests() 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 12e0038c6c..36312c8fdb 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 @@ -223,4 +223,29 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Second OnWithTimedOff cancels existing timer and resets motion", + function() + local frm_ctrl = FrameCtrl(0x01) + -- Pre-register two timers: first will be cancelled, second will fire + test.timer.__create_and_queue_test_time_advance_timer(0x0708/10, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(0x0708/10, "oneshot") + -- First motion event + local first_cmd = OnOff.server.commands.OnWithTimedOff.build_test_rx(mock_device, 0x00, 0x0708, 0x0000) + first_cmd.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({mock_device.id, first_cmd}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.wait_for_events() + -- Second motion event before first timer fires - cancels first timer + local second_cmd = OnOff.server.commands.OnWithTimedOff.build_test_rx(mock_device, 0x00, 0x0708, 0x0000) + second_cmd.body.zcl_header.frame_ctrl = frm_ctrl + test.socket.zigbee:__queue_receive({mock_device.id, second_cmd}) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + test.wait_for_events() + -- Only the second timer fires + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) + end +) + test.run_registered_tests() 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 ddeaf77ce4..7a1655fc41 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 @@ -140,4 +140,23 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Handle added lifecycle - reads ApplicationVersion", + { + { + channel = "device_lifecycle", + direction = "receive", + message = {mock_device1.id, "added"} + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device1.id, + Basic.attributes.ApplicationVersion:read(mock_device1) + } + } + } +) + test.run_registered_tests() 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 5308e11ee2..a148df5b1e 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 @@ -7,6 +7,7 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local constants = require "st.zigbee.constants" -- Mock out globals @@ -222,4 +223,99 @@ test.register_coroutine_test( end ) +test.register_message_test( + "resetEnergyMeter command should send OnOff On to reset device", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "energyMeter", component = "main", command = "resetEnergyMeter", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.OnOff.server.commands.On(mock_device) } + } + } +) + +test.register_coroutine_test( + "refresh capability command should read device 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, + SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_attribute(mock_device, data_types.ClusterId(SimpleMetering.ID), data_types.AttributeId(0x0001)) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSVoltage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.RMSCurrent:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + end +) + +test.register_coroutine_test( + "energy handler resets offset when reading is below stored offset", + function() + -- Set an offset larger than the incoming value (100 raw / 100 = 1.0 kWh, offset = 5.0) + mock_device:set_field(constants.ENERGY_METER_OFFSET, 5.0, {persist = true}) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- Offset resets to 0; raw_value_kilowatts = 1.0 - 0 = 1.0; no powerConsumptionReport (delta_tick < 15 min) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.0, unit = "kWh"})) + ) + end +) + +test.register_coroutine_test( + "energy handler resets save tick when timer has slipped beyond 30 minutes", + function() + -- Advance time > 30 min so that curr_save_tick + 15*60 < os.time() is true + test.timer.__create_and_queue_test_time_advance_timer(40*60, "oneshot") + test.mock_time.advance_time(40*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- raw_value = 100, divisor = 100, kWh = 1.0, watts = 1000.0; first report: deltaEnergy = 0.0 + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1000.0, deltaEnergy = 0.0})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.0, unit = "kWh"})) + ) + end +) + test.run_registered_tests() \ No newline at end of file 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 a4b8b8c037..2949961fb1 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 @@ -26,6 +26,7 @@ local zb_const = require "st.zigbee.constants" local zcl_messages = require "st.zigbee.zcl" local data_types = require "st.zigbee.data_types" local Status = require "st.zigbee.generated.types.ZclStatus" +local constants = require "st.zigbee.constants" local mock_device = test.mock_device.build_test_zigbee_device( @@ -228,6 +229,41 @@ test.register_coroutine_test( end } ) +test.register_coroutine_test( + "energy handler resets shinasystems offset when reading is below stored offset", + function() + -- divisor=1000; raw_value=100 -> 0.1 kWh; offset=0.5 -> 0.1 < 0.5 triggers reset + mock_device:set_field(constants.ENERGY_METER_OFFSET, 0.5, {persist = true}) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 100) + }) + -- offset resets to 0; raw_value_kilowatts = 0.1; no powerConsumptionReport (delta_tick < 15 min) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 0.1, unit = "kWh"})) + ) + end +) + +test.register_coroutine_test( + "shinasystems energy handler resets save tick when timer has slipped beyond 30 minutes", + function() + -- Advance time > 30 min so that curr_save_tick + 15*60 < os.time() is true + test.timer.__create_and_queue_test_time_advance_timer(40*60, "oneshot") + test.mock_time.advance_time(40*60) + test.socket.zigbee:__queue_receive({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 1500) + }) + -- raw_value=1500, divisor=1000, kWh=1.5, watts=1500.0; first report: deltaEnergy=0.0 + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerConsumptionReport.powerConsumption({energy = 1500.0, deltaEnergy = 0.0})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.energyMeter.energy({value = 1.5, unit = "kWh"})) + ) + end +) test.run_registered_tests() 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 new file mode 100644 index 0000000000..81caddae56 --- /dev/null +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua @@ -0,0 +1,102 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local constants = require "st.zigbee.constants" + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("power-meter.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Develco Products A/S", + model = "EMIZB-132", + server_clusters = {SimpleMetering.ID, ElectricalMeasurement.ID} + } + } +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + mock_device:set_field("_configuration_version", 1, {persist = true}) + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "frient device_init sets divisor fields", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + assert(mock_device:get_field(constants.SIMPLE_METERING_DIVISOR_KEY) == 1000, + "SIMPLE_METERING_DIVISOR_KEY should be 1000") + assert(mock_device:get_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY) == 10000, + "ELECTRICAL_MEASUREMENT_DIVISOR_KEY should be 10000") + end +) + +test.register_coroutine_test( + "frient lifecycle configure event should configure device", + function() + 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, + SimpleMetering.attributes.InstantaneousDemand: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, + ElectricalMeasurement.attributes.ActivePower:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMetering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurement.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + ElectricalMeasurement.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.run_registered_tests() 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 6957148647..ca98e8a1a7 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 @@ -10,6 +10,7 @@ local PowerConfiguration = clusters.PowerConfiguration local capabilities = require "st.capabilities" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local presence_utils = require "presence_utils" -- Needed for building ConfigureReportingResponse msg local messages = require "st.zigbee.messages" @@ -294,4 +295,96 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "battery_config_response_handler cancels pre-existing recurring poll timer", + function() + -- Place a live timer in the field so the nil-check branch is taken. + local pre_timer = test.timer.__create_test_time_advance_timer(60, "interval") + mock_simple_device:set_field(presence_utils.RECURRING_POLL_TIMER, pre_timer) + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + build_config_response_msg(PowerConfiguration.ID, 0x00) + }) + -- poke() emits "present" for every inbound zigbee message + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present")) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "info_changed with changed check_interval cancels existing recurring poll timer", + function() + local pre_timer = test.timer.__create_test_time_advance_timer(60, "interval") + mock_simple_device:set_field(presence_utils.RECURRING_POLL_TIMER, pre_timer) + test.socket.device_lifecycle():__queue_receive( + mock_simple_device:generate_info_changed({ preferences = { check_interval = 100 } }) + ) + test.wait_for_events() + end +) + +-- Build two additional mock devices (module-level) for checkInterval type variants. +-- The profile default sets checkInterval = 120 (number); we override after building. +local mock_device_str_interval = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("smartthings-arrival-sensor.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SmartThings", + model = "tagv4", + server_clusters = {0x0000, 0x0001, 0x0003} + } + } + } +) +mock_device_str_interval.preferences.checkInterval = "120" -- string → triggers elseif branch + +local mock_device_nil_interval = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("smartthings-arrival-sensor.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SmartThings", + model = "tagv4", + server_clusters = {0x0000, 0x0001, 0x0003} + } + } + } +) +mock_device_nil_interval.preferences.checkInterval = nil -- nil → triggers default-return branch + +test.register_coroutine_test( + "init with string checkInterval uses parsed value for presence timeout", + function() + test.mock_device.add_test_device(mock_device_str_interval) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device_str_interval.id, "init" }) + test.wait_for_events() + test.mock_time.advance_time(121) + test.socket.capability:__expect_send( + mock_device_str_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) + ) + end, + { test_init = function() end } +) + +test.register_coroutine_test( + "init with nil checkInterval uses default presence timeout", + function() + test.mock_device.add_test_device(mock_device_nil_interval) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.device_lifecycle:__queue_receive({ mock_device_nil_interval.id, "init" }) + test.wait_for_events() + test.mock_time.advance_time(121) + test.socket.capability:__expect_send( + mock_device_nil_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) + ) + end, + { test_init = function() end } +) + test.run_registered_tests() 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 4aa03d2bca..bdd79dd59f 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 @@ -174,4 +174,36 @@ test.register_message_test( } ) +test.register_message_test( + "ZoneStatusChangeNotification - mains should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "Device added lifecycle should emit mains powerSource", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_device.id, "added" } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + test.run_registered_tests() 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 8140da64bd..c88d272807 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -1001,4 +1001,120 @@ test.register_message_test( } ) +local function build_sw_version_attr_report(device, fw_bytes) + local zcl_messages_mod = require "st.zigbee.zcl" + local messages_mod = require "st.zigbee.messages" + local zb_const_mod = require "st.zigbee.constants" + local report_attr = require "st.zigbee.zcl.global_commands.report_attribute" + local zcl_cmds_mod = require "st.zigbee.zcl.global_commands" + + local attr_record = report_attr.ReportAttributeAttributeRecord( + DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR, + data_types.CharString.ID, + fw_bytes + ) + local report_body = report_attr.ReportAttribute({ attr_record }) + local zclh = zcl_messages_mod.ZclHeader({ + cmd = data_types.ZCLCommandId(zcl_cmds_mod.REPORT_ATTRIBUTE_ID) + }) + local addrh = messages_mod.AddressHeader( + device:get_short_address(), + device:get_endpoint(Basic.ID), + zb_const_mod.HUB.ADDR, + zb_const_mod.HUB.ENDPOINT, + zb_const_mod.HA_PROFILE_ID, + Basic.ID + ) + local message_body = zcl_messages_mod.ZclMessageBody({ zcl_header = zclh, zcl_body = report_body }) + return messages_mod.ZigbeeMessageRx({ address_header = addrh, body = message_body }) +end + +test.register_coroutine_test( + "SW version attr handler with new firmware should configure battery percentage", + function() + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + -- Binary "\x01\x09\x03" -> hex "010903" (new firmware >= SIREN_FIXED_ENDIAN_SW_VERSION) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_sw_version_attr_report(mock_device, "\x01\x09\x03") + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SW version attr handler with old firmware should configure battery voltage", + function() + mock_device:set_field(PRIMARY_SW_VERSION, nil, {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + -- Binary "\x01\x09\x01" -> hex "010901" (old firmware < SIREN_FIXED_ENDIAN_SW_VERSION) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_sw_version_attr_report(mock_device, "\x01\x09\x01") + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "doConfigure with existing sw_version should call configure_battery_handling", + function() + mock_device:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device:set_field("_frient_battery_config_applied", nil, {persist = true}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASWD.attributes.MaxDuration:write(mock_device, ALARM_DEFAULT_MAX_DURATION) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting( + mock_device, 30, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + 0x2B + ):to_endpoint(0x2B) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0, 21600, 1) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse(mock_device, IasEnrollResponseCode.SUCCESS, 0x00) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() \ No newline at end of file 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 810e691849..aed318f49a 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 @@ -64,6 +64,24 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) +local mock_device_112 = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("frient-siren-battery-source-tamper.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "SIRZB-112", + server_clusters = { Scenes.ID, OnOff.ID} + }, + [0x2B] = { + id = 0x2B, + server_clusters = { Basic.ID, Identify.ID, PowerConfiguration.ID, Groups.ID, IASZone.ID, IASWD.ID } + } + } + } +) + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device) @@ -1026,4 +1044,40 @@ test.register_message_test( } ) +local function get_siren_OFF_commands_112(duration) + local expectedSirenOFFConfiguration = SirenConfiguration(0x00) + expectedSirenOFFConfiguration:set_warning_mode(WarningMode.STOP) + expectedSirenOFFConfiguration:set_siren_level(IaswdLevel.LOW_LEVEL) + + test.socket.zigbee:__expect_send({ + mock_device_112.id, + IASWD.server.commands.StartWarning( + mock_device_112, + expectedSirenOFFConfiguration, + data_types.Uint16(duration and duration or ALARM_DURATION_TEST_VALUE), + data_types.Uint8(0x00), + data_types.Enum8(0x00) + ) + }) +end + +test.register_coroutine_test( + "SIRZB-112 alarm off command should be handled via frient sub-driver", + function() + mock_device_112:set_field(PRIMARY_SW_VERSION, "010903", {persist = true}) + mock_device_112:set_field(ALARM_DURATION, ALARM_DURATION_TEST_VALUE, {persist = true}) + + test.socket.capability:__queue_receive({ + mock_device_112.id, + { capability = "alarm", component = "main", command = "off", args = {} } + }) + + get_siren_OFF_commands_112() + test.wait_for_events() + end, + { test_init = function() + test.mock_device.add_test_device(mock_device_112) + end } +) + test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua index 3dc707a297..52db9f96f4 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_zigbee_siren.lua @@ -340,6 +340,63 @@ test.register_coroutine_test( end ) +local function build_default_response_zigbee_msg() + local zcl_messages = require "st.zigbee.zcl" + local messages = require "st.zigbee.messages" + local zb_const = require "st.zigbee.constants" + local buf_lib = require "st.buf" + local buf_from_str = function(str) return buf_lib.Reader(str) end + local frame = "\x00\x00" + local default_response = zcl_cmds.DefaultResponse.deserialize(buf_from_str(frame)) + local zclh = zcl_messages.ZclHeader({ cmd = data_types.ZCLCommandId(zcl_cmds.DefaultResponse.ID) }) + local addrh = messages.AddressHeader( + mock_device:get_short_address(), + mock_device:get_endpoint(data_types.ClusterId(IASWD.ID)), + zb_const.HUB.ADDR, zb_const.HUB.ENDPOINT, zb_const.HA_PROFILE_ID, IASWD.ID) + local message_body = zcl_messages.ZclMessageBody({ zcl_header = zclh, zcl_body = default_response }) + return messages.ZigbeeMessageRx({ address_header = addrh, body = message_body }) +end + +test.register_coroutine_test( + "Default response with OFF alarm command should emit off events", + function() + mock_device:set_field("alarmCommand", 0, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + end +) + +test.register_coroutine_test( + "Default response with STROBE alarm command should emit strobe event and timer off", + function() + test.timer.__create_and_queue_test_time_advance_timer(180, "oneshot") + mock_device:set_field("alarmCommand", 2, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.strobe())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Default response with BOTH alarm command should emit both event", + function() + test.timer.__create_and_queue_test_time_advance_timer(180, "oneshot") + mock_device:set_field("alarmCommand", 3, {persist = true}) + test.socket.zigbee:__queue_receive({ mock_device.id, build_default_response_zigbee_msg() }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.both())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) + test.mock_time.advance_time(180) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.alarm.alarm.off())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) + test.wait_for_events() + end +) + test.register_coroutine_test( "Setting a max duration should be handled", function() 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 4c1dcb1552..1203ffa00a 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 @@ -244,6 +244,66 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "selfCheck report should be handled, idle", + function() + local attr_report_data = { + { PRIVATE_SELF_CHECK_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + selfCheck.selfCheckState.idle())) + end +) + +test.register_coroutine_test( + "sensitivityAdjustment report should be handled, High", + function() + local attr_report_data = { + { PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High())) + end +) + +test.register_coroutine_test( + "Capability on command should be handled : setSensitivityAdjustment High", + function() + local attr_report_data = { + { PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, data_types.Uint8.ID, 0x02 } + } + test.socket.capability:__queue_receive({ mock_device.id, + { capability = sensitivityAdjustmentId, component = "main", command = "setSensitivityAdjustment", args = {"High"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + PRIVATE_SENSITIVITY_ADJUSTMENT_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x02) + }) + test.wait_for_events() + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High()) + ) + test.wait_for_events() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = sensitivityAdjustmentId, component = "main", command = "setSensitivityAdjustment", args = {"High"}} + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + sensitivityAdjustment.sensitivityAdjustment.High()) + ) + end +) test.register_coroutine_test( 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 1af67b36cd..b0b6a4875a 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 @@ -7,13 +7,10 @@ local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" - - local PowerConfiguration = clusters.PowerConfiguration local selfCheck = capabilities["stse.selfCheck"] local selfCheckId = "stse.selfCheck" - test.add_package_capability("selfCheck.yaml") local PRIVATE_CLUSTER_ID = 0xFCC0 @@ -23,8 +20,6 @@ local PRIVATE_MUTE_ATTRIBUTE_ID = 0x0126 local PRIVATE_SELF_CHECK_ATTRIBUTE_ID = 0x0127 local PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID = 0x013A - - local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("smoke-battery-aqara.yml"), @@ -39,15 +34,11 @@ 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.set_test_init_function(test_init) test.register_coroutine_test( @@ -89,7 +80,6 @@ test.register_coroutine_test( end ) - test.register_coroutine_test( "smokeDetector report should be handled", function() @@ -105,8 +95,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "audioMute report should be handled", function() @@ -122,8 +110,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "Capability on command should be handled : device mute", function() @@ -135,8 +121,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "selfCheck report should be handled", function() @@ -152,8 +136,6 @@ test.register_coroutine_test( end ) - - test.register_coroutine_test( "Capability on command should be handled : device selfCheck", function() @@ -166,7 +148,50 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "smokeDetector report should be handled, smoke clear", + function() + local attr_report_data = { + { PRIVATE_SMOKE_ZONE_STATUS_ATTRIBUTE_ID, data_types.Uint16.ID, 0x0000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.smokeDetector.smoke.clear())) + end +) + +test.register_coroutine_test( + "audioMute report should be handled, unmuted", + function() + local attr_report_data = { + { PRIVATE_MUTE_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.audioMute.mute.unmuted())) + end +) +test.register_coroutine_test( + "selfCheck report should be handled, idle", + function() + local attr_report_data = { + { PRIVATE_SELF_CHECK_ATTRIBUTE_ID, data_types.Uint8.ID, 0x00 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + selfCheck.selfCheckState.idle())) + end +) test.register_message_test( "Battery voltage report should be handled", @@ -184,5 +209,4 @@ test.register_message_test( } ) - test.run_registered_tests() 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 364aae57a0..21b1660c1a 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 @@ -569,4 +569,36 @@ test.register_coroutine_test( end ) +test.register_message_test( + "IASZone attribute report should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.heat()) + } + } +) + +test.register_coroutine_test( + "IASZone attribute report should be handled: cleared", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) + }) + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() 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 fa012a321b..fdb05cf22d 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 @@ -28,6 +28,12 @@ local DEVELCO_BASIC_PRIMARY_SW_VERSION_ATTR = 0x8000 local DEVELCO_MANUFACTURER_CODE = 0x1015 local cluster_base = require "st.zigbee.cluster_base" local defaultWarningDuration = 240 +local messages = require "st.zigbee.messages" +local default_response = require "st.zigbee.zcl.global_commands.default_response" +local zb_const = require "st.zigbee.constants" +local Status = require "st.zigbee.generated.types.ZclStatus" +local zcl_messages = require "st.zigbee.zcl" +local ALARM_COMMAND = "alarmCommand" local mock_device = test.mock_device.build_test_zigbee_device( @@ -609,4 +615,88 @@ test.register_coroutine_test( end ) +local function build_default_response_msg(device, cluster, command, status) + local addr_header = messages.AddressHeader( + device:get_short_address(), + device.fingerprinted_endpoint_id, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + cluster + ) + local default_response_body = default_response.DefaultResponse(command, status) + local zcl_header = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(default_response_body.ID) + }) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_header, + zcl_body = default_response_body + }) + return messages.ZigbeeMessageRx({ + address_header = addr_header, + body = message_body + }) +end + +test.register_message_test( + "IASZone attribute report should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", smokeDetector.smoke.detected()) + } + } +) + +test.register_coroutine_test( + "IASZone attribute report should be handled: clear", + function() + test.timer.__create_and_queue_test_time_advance_timer(6, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) + }) + test.mock_time.advance_time(6) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", smokeDetector.smoke.clear()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "default_response_handler: siren active, emit siren then off after delay", + function() + mock_device:set_field(ALARM_COMMAND, 1) + test.timer.__create_and_queue_test_time_advance_timer(ALARM_DEFAULT_MAX_DURATION, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(mock_device, IASWD.ID, IASWD.server.commands.StartWarning.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.siren())) + test.mock_time.advance_time(ALARM_DEFAULT_MAX_DURATION) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.off())) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "default_response_handler: alarm off, emit off", + function() + mock_device:set_field(ALARM_COMMAND, 0) + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(mock_device, IASWD.ID, IASWD.server.commands.StartWarning.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", alarm.alarm.off())) + test.wait_for_events() + end +) + test.run_registered_tests() 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 c521ba3e44..516a18c326 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -180,4 +180,38 @@ test.register_coroutine_test( end ) +local mock_device_cwacn1 = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("aqara-light.yml"), + preferences = { ["stse.lightFadeInTimeInSec"] = 0, ["stse.lightFadeOutTimeInSec"] = 0 }, + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "LUMI", + model = "lumi.light.cwacn1", + server_clusters = { 0x0006, 0x0008, 0x0300 } + } + } + } +) + +test.register_coroutine_test( + "Handle added lifecycle for lumi.light.cwacn1 model (colorTemperatureRange max = 6500)", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device_cwacn1.id, "added" }) + + test.socket.zigbee:__expect_send({ + mock_device_cwacn1.id, cluster_base.write_manufacturer_specific_attribute(mock_device_cwacn1, PRIVATE_CLUSTER_ID, + PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device_cwacn1:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6500 }))) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_device_cwacn1) + end + } +) + test.run_registered_tests() 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 d37dafe45d..bd54b1b20f 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 @@ -138,4 +138,38 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle infoChanged when dimOnOff changes from 1 to 0 should write transition time 0", + function() + -- First: change dimOnOff from default 0 to 1 (triggers write with dimRate=20) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 20) }) + test.wait_for_events() + -- Now: change dimOnOff from 1 to 0 (driver old=1, new=0 -> writes 0) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 0 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 0) }) + end +) + +test.register_coroutine_test( + "Handle infoChanged when dimRate changes while dimOnOff is 1 should write new dimRate", + function() + -- First: change dimOnOff from default 0 to 1 (triggers write with dimRate=20) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 20) }) + test.wait_for_events() + -- Now: change dimRate while dimOnOff stays 1 (driver old={dimOnOff=1,dimRate=20}, new={dimOnOff=1,dimRate=50}) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { dimOnOff = 1, dimRate = 50 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 50) }) + end +) + test.run_registered_tests() 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 52ba3ef2a6..d6dcc08ea4 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -5,11 +5,14 @@ local test = require "integration_test" local clusters = require "st.zigbee.zcl.clusters" local OnOffCluster = clusters.OnOff local SimpleMeteringCluster = clusters.SimpleMetering +local ElectricalMeasurementCluster = clusters.ElectricalMeasurement local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("switch-power-energy.yml"), + fingerprinted_endpoint_id = 0x01, zigbee_endpoints = { [1] = { id = 1, @@ -21,6 +24,8 @@ local mock_device = test.mock_device.build_test_zigbee_device({ } }) +zigbee_test_utils.prepare_zigbee_env_info() + local function test_init() mock_device:set_field("_configuration_version", 1, {persist = true}) test.mock_device.add_test_device(mock_device) @@ -113,4 +118,30 @@ test.register_message_test( } ) +test.register_coroutine_test( + "doConfigure lifecycle should call refresh and configure on device", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + -- device:refresh() reads + test.socket.zigbee:__expect_send({ mock_device.id, OnOffCluster.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) }) + -- device:configure() bind requests and configure_reporting + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, OnOffCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, SimpleMeteringCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, ElectricalMeasurementCluster.ID) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() 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 0457c0eb62..3b004c8f91 100755 --- a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua @@ -458,4 +458,21 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "component main Capability on command emits on then off after delay", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.on()) + ) + test.wait_for_events() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.off()) + ) + end +) + test.run_registered_tests() 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 0f00d47fdb..b4ded05175 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 @@ -476,4 +476,45 @@ test.register_coroutine_test( end ) +local mock_non_mns_device = test.mock_device.build_test_zigbee_device( + { + label = "Generic Switch 1", + profile = profile, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "GenericMfr", + model = "GenericModel-DualSwitch", + server_clusters = { 0x0006 } + }, + [2] = { + id = 2, + manufacturer = "GenericMfr", + model = "GenericModel-DualSwitch", + server_clusters = { 0x0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + +test.register_coroutine_test( + "device added lifecycle creates child device for secondary OnOff endpoint", + function() + test.socket.device_lifecycle:__queue_receive({ mock_non_mns_device.id, "added" }) + mock_non_mns_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Generic Switch 1 2", + profile = "basic-switch", + parent_device_id = mock_non_mns_device.id, + parent_assigned_child_key = "02" + }) + end, + { + test_init = function() + test.mock_device.add_test_device(mock_non_mns_device) + end + } +) + test.run_registered_tests() 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 ce1a9e2882..1a8d42dba5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua @@ -118,6 +118,23 @@ local mock_seventh_child = test.mock_device.build_test_child_device( } ) +-- Single button device matching WALL HERO fingerprint (used to test button capability events) +local mock_button_device = test.mock_device.build_test_zigbee_device( + { + label = "WALL HERO Switch 1", + profile = scene_switch_profile_def, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "WALL HERO", + model = "ACL-401S1I", + server_clusters = { 0x0003, 0x0004, 0x0005, 0x0006 } + } + }, + fingerprinted_endpoint_id = 0x01 + } +) + zigbee_test_utils.prepare_zigbee_env_info() local function test_init() @@ -129,7 +146,9 @@ local function test_init() test.mock_device.add_test_device(mock_fourth_child) test.mock_device.add_test_device(mock_fifth_child) test.mock_device.add_test_device(mock_sixth_child) - test.mock_device.add_test_device(mock_seventh_child)end + test.mock_device.add_test_device(mock_seventh_child) + test.mock_device.add_test_device(mock_button_device) +end test.set_test_init_function(test_init) @@ -710,4 +729,15 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "device_added lifecycle event should emit button capability events for button device", + function() + test.socket.device_lifecycle:__queue_receive({ mock_button_device.id, "added" }) + test.socket.capability:__expect_send(mock_button_device:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_button_device:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) + end +) + test.run_registered_tests() 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 0717fbb9fa..74cc7a2115 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua @@ -174,4 +174,31 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Refresh should send read requests for all necessary 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, + Thermostat.attributes.LocalTemperature:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device) + }) + end +) + test.run_registered_tests() 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 afb36720a3..4ff17803fa 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 @@ -517,5 +517,51 @@ test.register_coroutine_test( }) end ) +test.register_coroutine_test( + "LocalTemperature handler should request PIHeatingDemand when setpoint > temperature", + function() + local RAW_SETPOINT_FIELD = "raw_setpoint" + mock_device:set_field(RAW_SETPOINT_FIELD, 3000, { persist = true }) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.LocalTemperature:build_test_attr_report(mock_device, 2000) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared()) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.PIHeatingDemand:read(mock_device) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 20.0, unit = "C" })) + ) + end +) + +test.register_coroutine_test( + "Setting an unsupported thermostat mode should re-emit the current mode", + function() + -- Establish a known current mode state + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, ThermostatSystemMode.OFF) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", ThermostatMode.thermostatMode.off()) + ) + test.wait_for_events() + + -- "cool" is not in SUPPORTED_MODES for stelpro-ki; the driver re-emits the current mode + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = { "cool" } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", ThermostatMode.thermostatMode.off()) + ) + end +) test.run_registered_tests() 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 638f3f7ff4..0886a7cd7f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -450,4 +450,44 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Setting cooling setpoint while in heat mode should re-emit the current cooling setpoint", + function() + -- 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() + + -- Set a known cooling setpoint state + 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() + + -- Try to set cooling setpoint while in heat mode; driver defers and re-emits current + 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 = { 27 } } + }) + test.wait_for_events() + + test.mock_time.advance_time(10) + -- After the delay, update_device_setpoint re-emits the unchanged cooling setpoint (25.0 C) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 25.0, unit = "C" })) + ) + test.wait_for_events() + end +) + test.run_registered_tests() 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 e925491c87..083d522bb3 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -82,6 +82,22 @@ test.register_message_test( } ) +test.register_message_test( + "Battery percentage report - not low should return 50%", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0000) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(50)) + } + } +) + test.register_message_test( "PowerSource(unknown) reporting should be handled", { 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 8710cef0c9..d30c60ddc6 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua @@ -131,4 +131,12 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Battery voltage above max should clamp to 100 percent", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 65) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) + end +) + test.run_registered_tests() 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 86e4aa774a..082c9fb97c 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 @@ -79,27 +79,15 @@ test.register_coroutine_test( end ) --- test.register_coroutine_test( --- "Health check should check all relevant attributes", --- function() --- test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) --- test.socket.capability:__expect_send( --- { --- mock_device.id, --- { --- capability_id = "waterSensor", component_id = "main", --- attribute_id = "water", state={value="dry"} --- } --- } --- ) --- end, --- { --- test_init = function() --- test.mock_device.add_test_device(mock_device) --- test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "health_check") --- end --- } --- ) +test.register_coroutine_test( + "Added lifecycle should emit water dry event", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.waterSensor.water.dry()) + ) + end +) test.register_coroutine_test( "Configure should configure all necessary attributes", 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 b52884f6a3..63be06f0a8 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 @@ -36,6 +36,14 @@ end test.set_test_init_function(test_init) +test.register_coroutine_test( + "Added lifecycle should read ApplicationVersion", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + test.socket.zigbee:__expect_send({mock_device.id, Basic.attributes.ApplicationVersion:read(mock_device)}) + end +) + test.register_coroutine_test( "Refresh necessary attributes", function() 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 35ab4d915c..1ad780782c 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 @@ -218,4 +218,50 @@ test.register_coroutine_test( end ) +test.register_message_test( + "ZoneStatusChangeNotification should be handled: detected", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0001, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.detected()) + } + } +) + +test.register_message_test( + "ZoneStatusChangeNotification should be handled: clear", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.client.commands.ZoneStatusChangeNotification.build_test_rx(mock_device, 0x0000, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.hardwareFault.hardwareFault.clear()) + } + } +) + +test.register_coroutine_test( + "fanspeed reported should be clamped to 0 when value >= 1000", + function() + local attr_report_data = { + { WATERING_TIME_ATTR, data_types.Uint16.ID, 1000 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, THIRDREALITY_WATERING_CLUSTER, attr_report_data, 0x1407) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanSpeed.fanSpeed(0))) + end +) + test.run_registered_tests() 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 a73bb6c5a3..8c87907886 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 @@ -215,4 +215,114 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Cancel existing set-status timer when a new partial level report arrives", + function() + -- First attr: level 90 sets T1 + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + 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 = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr arrives before T1 fires: should cancel T1 and create T2 + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 15) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 85 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + -- T2 fires; T1 was cancelled so only partially_open from T2 + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Timer callback emits closed when shade reaches level 0", + function() + -- First attr starts partial movement and arms a 1-second status timer + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + 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 = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr reports fully closed (level=0); goes through elseif branch, T1 still pending + 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(0)) + ) + -- T1 fires; get_latest_state returns 0 so the callback emits closed() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Timer callback emits open when shade reaches level 100", + function() + -- First attr starts partial movement and arms a 1-second status timer + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + 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 = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr reports fully open (level=100); goes through elseif branch, T1 still pending + 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(100)) + ) + -- T1 fires; get_latest_state returns 100 so the callback emits open() + test.mock_time.advance_time(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua index c13164df09..725fda3f62 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_yoolax.lua @@ -322,4 +322,109 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Default response emits opening when current level is higher than target", + function() + -- Establish a partially-closed shade state (zigbee value 90 → shadeLevel 10) + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 90) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(10)) + ) + test.wait_for_events() + -- Send open command: MOST_RECENT_SETLEVEL = 0 (level = 100 - 100 = 0) + 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.GoToLiftPercentage(mock_device, 0) + }) + test.wait_for_events() + -- Default response: current_level_zigbee=90, most_recent=0 → 90 > 0 → opening() + test.socket.zigbee:__queue_receive({ + mock_device.id, + build_default_response_msg(WindowCovering.ID, WindowCovering.server.commands.GoToLiftPercentage.ID, Status.SUCCESS) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Attr handler emits partially_open when report matches most-recent set level", + function() + -- Send presetPosition; preset level = 50 so MOST_RECENT_SETLEVEL = 50 (100-50=50) + 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() + -- Attr report value=50 matches MOST_RECENT_SETLEVEL; shade stops at partial level + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + -- current_level was nil → partially_open from the nil-check branch + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + -- most_recent matches and value is partial → partially_open again (from the match branch) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50)) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Spontaneous level report towards open emits opening event", + function() + -- First attr establishes a partial shade level (value=10 → shadeLevel=90) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(90)) + ) + -- Second attr moves toward open (value=5 < current zigbee 10 → opening) + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 5) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(95)) + ) + test.wait_for_events() + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() 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 6c8ea27d01..fa764a5db0 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 @@ -517,4 +517,96 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Attribute handler reports closed when level is 0", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 0 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + } + } +) + +test.register_message_test( + "Attribute handler reports open when level is 100", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 100 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + } + } +) + +test.register_coroutine_test( + "Cancel existing poll timer when a new partial level report arrives", + function() + -- First attr: level 90 creates T1 via overwrite_existing_timer_if_needed + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + 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 = 90 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + -- Second attr before T1 fires: overwrite_existing_timer_if_needed cancels T1 and stores T2 + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__queue_receive({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 15) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 85 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + -- T2 fires; T1 was cancelled so only one partially_open + test.mock_time.advance_time(2) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua index ae87438c99..37841524c4 100755 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_vimar.lua @@ -315,4 +315,122 @@ test.register_coroutine_test( end ) +test.register_message_test( + "Attribute handler reports closed when shade reaches level 0", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 0 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + } + } +) + +test.register_message_test( + "Attribute handler reports open when shade reaches level 100", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + }, + { + channel = "capability", + direction = "send", + message = { + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 100 } } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + } + } +) + +test.register_coroutine_test( + "SetLevel command emits closing when requested level is below current level", + function() + -- Attr report sets current shade level to 90 (inverted value=10) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + 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 = 90 } } + }) + test.wait_for_events() + -- Now both vimar flags are false; requesting level 30 (< 90) triggers closing branch + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 30 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(30)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 70) + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "SetLevel command is ignored (early return) when shades are already moving", + function() + -- Open command: current=0 < 100 → opening, sets VIMAR_SHADES_OPENING=true + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + test.wait_for_events() + -- While opening, a different setShadeLevel is requested: ignored with current level re-emitted + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 } } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.wait_for_events() + end +) + test.run_registered_tests() From eb70a7cc00f028ba655990e503cfa077739044de Mon Sep 17 00:00:00 2001 From: Taejun Park Date: Fri, 27 Feb 2026 11:12:44 +0900 Subject: [PATCH 444/449] Change the icon for the Matter Ikea knob and dual button --- .../matter-switch/fingerprints.yml | 2 +- .../profiles/ikea-2-button-battery.yml | 53 +++++++++++++++++++ .../matter-switch/profiles/ikea-scroll.yml | 46 ++++++++++++++++ .../src/switch_utils/device_configuration.lua | 3 ++ .../matter-switch/src/switch_utils/fields.lua | 3 +- 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 33f1b6f1d4..1c8adddff4 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -867,7 +867,7 @@ matterManufacturer: deviceLabel: BILRESA dual button vendorId: 0x117C productId: 0x8001 - deviceProfileName: 2-button-battery + deviceProfileName: ikea-2-button-battery #Innovation Matters - id: "4978/1" deviceLabel: M2D Bridge diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml b/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml new file mode 100644 index 0000000000..0b256f3b65 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/ikea-2-button-battery.yml @@ -0,0 +1,53 @@ +name: ikea-2-button-battery +components: + - id: main + label: Button 1 + capabilities: + - id: button + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Button 2 + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +deviceConfig: + icons: + - group: main + iconUrl: 'icon://button_multi' + dashboard: + states: + - component: main + capability: button + version: 1 + detailView: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: button2 + capability: button + version: 1 + automation: + conditions: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: button2 + capability: button + version: 1 + actions: [] diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index e07006ce9d..166b0a62f1 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -33,3 +33,49 @@ components: version: 1 categories: - name: Button +deviceConfig: + icons: + - group: main + iconUrl: 'icon://button_wheel' + dashboard: + states: + - component: main + capability: button + version: 1 + detailView: + - component: main + capability: button + version: 1 + - component: main + capability: knob + version: 1 + - component: main + capability: battery + version: 1 + - component: group2 + capability: button + version: 1 + - component: group2 + capability: knob + version: 1 + - component: group3 + capability: button + version: 1 + - component: group3 + capability: knob + version: 1 + automation: + conditions: + - component: main + capability: button + version: 1 + - component: main + capability: battery + version: 1 + - component: group2 + capability: button + version: 1 + - component: group3 + capability: button + version: 1 + actions: [] 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 8542972320..18219ef459 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -132,6 +132,9 @@ function ButtonDeviceConfiguration.update_button_profile(device, default_endpoin if switch_utils.get_product_override_field(device, "is_climate_sensor_w100") then profile_name = "3-button-battery-temperature-humidity" end + if switch_utils.get_product_override_field(device, "is_ikea_dual_button") then + profile_name = "ikea-2-button-battery" + end return profile_name end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 6eb03b1472..2b315c6528 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -101,7 +101,8 @@ SwitchFields.vendor_overrides = { [0x2004] = { is_climate_sensor_w100 = true }, -- Climate Sensor W100, requires unique profile }, [0x117C] = { -- IKEA_MANUFACTURER_ID - [0x8000] = { is_ikea_scroll = true } + [0x8000] = { is_ikea_scroll = true }, -- BILRESA scroll wheel + [0x8001] = { is_ikea_dual_button = true}, -- BILRESA dual button }, [0x1189] = { -- LEDVANCE_MANUFACTURER_ID [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, From 0298090b54fd3d56d76d4994673cd0c33e2e75b7 Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:32:59 -0600 Subject: [PATCH 445/449] Matter Camera: Remove trigger before zone (#2816) The server will return `INVALID_IN_STATE` if `RemoveZone` is issued while there is an existing trigger for the given zone. The trigger should be removed first before attempting to remove the zone. --- .../camera_handlers/capability_handlers.lua | 9 ++ .../src/test/test_matter_camera.lua | 93 ++++++++++++++++++- 2 files changed, 100 insertions(+), 2 deletions(-) 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 fb85eb863f..09134a9757 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 @@ -259,6 +259,15 @@ end function CameraCapabilityHandlers.handle_remove_zone(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) + local triggers = device:get_latest_state( + camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME + ) or {} + for _, v in pairs(triggers) do + if v.zoneId == cmd.args.zoneId then + device:send(clusters.ZoneManagement.server.commands.RemoveTrigger(device, endpoint_id, cmd.args.zoneId)) + break + end + end device:send(clusters.ZoneManagement.server.commands.RemoveZone(device, endpoint_id, cmd.args.zoneId)) 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 930cb069dc..facc6ea984 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -1573,7 +1573,7 @@ test.register_coroutine_test( test.socket.capability:__queue_receive({ mock_device.id, { capability = "zoneManagement", component = "main", command = "newZone", args = { - i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "blue" + i .. " zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}} }, i, "#FFFFFF" }} }) test.socket.matter:__expect_send({ @@ -1586,7 +1586,7 @@ test.register_coroutine_test( clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) }, - color = "blue" + color = "#FFFFFF" } ) ) @@ -1773,6 +1773,95 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Removing a zone with an existing trigger should send RemoveTrigger followed by RemoveZone", + function() + update_device_profile() + test.wait_for_events() + + -- Create a zone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "newZone", args = { + "motion zone", {{value = {x = 0, y = 0}}, {value = {x = 1920, y = 1080}}}, "motion", "#FFFFFF" + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateTwoDCartesianZone(mock_device, CAMERA_EP, + clusters.ZoneManagement.types.TwoDCartesianZoneStruct({ + name = "motion zone", + use = clusters.ZoneManagement.types.ZoneUseEnum.MOTION, + vertices = { + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 0, y = 0}), + clusters.ZoneManagement.types.TwoDCartesianVertexStruct({x = 1920, y = 1080}) + }, + color = "#FFFFFF" + }) + ) + }) + + -- Create a trigger + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3, + sensitivity = 5 + }) + }) + + -- Receive the Triggers attribute update from the device reflecting the new trigger + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Triggers:build_test_report_data( + mock_device, CAMERA_EP, { + clusters.ZoneManagement.types.ZoneTriggerControlStruct({ + zone_id = 1, initial_duration = 10, augmentation_duration = 3, + max_duration = 15, blind_duration = 3, sensitivity = 5 + }) + } + ) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.triggers({{ + zoneId = 1, initialDuration = 10, augmentationDuration = 3, + maxDuration = 15, blindDuration = 3, sensitivity = 5 + }})) + ) + test.wait_for_events() + + -- Receive removeZone command: since a trigger exists for zone 1, RemoveTrigger is sent first, then RemoveZone + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "zoneManagement", component = "main", command = "removeZone", args = { 1 } } + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveTrigger(mock_device, CAMERA_EP, 1) + }) + test.socket.matter:__expect_send({ + mock_device.id, clusters.ZoneManagement.server.commands.RemoveZone(mock_device, CAMERA_EP, 1) + }) + test.wait_for_events() + + -- Receive the updated Zones attribute from the device with the zone removed + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.ZoneManagement.attributes.Zones:build_test_report_data(mock_device, CAMERA_EP, {}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.zoneManagement.zones({value = {}})) + ) + end +) + test.register_coroutine_test( "Stream management commands should send the appropriate commands", function() From a5c8b2a769d81828e9639d6f1fa362ce1e622b94 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 3 Mar 2026 11:56:22 -0600 Subject: [PATCH 446/449] Fixed pre-commit script misread binaries and allowing for copyright symbol - Fixed issue where grep would read files as binaries and fail to parse them, so forcing reading of them as a text file - Added copyright logo to regex for better coverage of copyright notice detection --- tools/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/pre-commit b/tools/pre-commit index c9bc4d4d4f..41a9406bf5 100755 --- a/tools/pre-commit +++ b/tools/pre-commit @@ -28,7 +28,7 @@ for file in $files; do # Check if file is not empty, is a SmartThings driver and has the copyright header if [ -s "$file" ] && [[ "$file" =~ "drivers/SmartThings" ]] \ - && [[ ! $(grep $file -P -e "-{2,} Copyright \d{4}(?:-\d{4})? SmartThings") ]]; then + && [[ ! $(grep $file --text -P -e "-{2,} Copyright (?:© )?\d{4}(?:-\d{4})? SmartThings") ]]; then echo "$file: SmartThings Copyright missing from file" fail=1 fi From 1437942a1ec78f5945d32e9a22a83bbb4f9af0be Mon Sep 17 00:00:00 2001 From: Konrad K <33450498+KKlimczukS@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:39:44 +0100 Subject: [PATCH 447/449] fingerprints for Zooz sensors: ZSE11 (WWSTCERT-10463) , ZSE41 (WWSTCERT-10451), ZSE44 (WWSTCERT-10455) (#2794) * fingerprints for Zooz sensors: ZSE11, ZSE41, ZSE44 * removes redundant white space * cleanup * decimal -> hex --- .../SmartThings/zwave-sensor/fingerprints.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zwave-sensor/fingerprints.yml b/drivers/SmartThings/zwave-sensor/fingerprints.yml index 502e5bd5a7..0810b20a38 100644 --- a/drivers/SmartThings/zwave-sensor/fingerprints.yml +++ b/drivers/SmartThings/zwave-sensor/fingerprints.yml @@ -446,11 +446,23 @@ zwaveManufacturer: productId: 0x000B deviceProfileName: contact-battery-tamperalert - id: 027A/7000/E001 - deviceLabel: Zooz Open/Closed Sensor + deviceLabel: Open Close XS Sensor manufacturerId: 0x027A productType: 0x7000 productId: 0xE001 - deviceProfileName: contact-battery-tamperalert + deviceProfileName: base-contact + - id: 027A/0201/0006 + deviceLabel: Q Sensor + manufacturerId: 0x027A + productType: 0x0201 + productId: 0x0006 + deviceProfileName: multi-functional-motion + - id: 027A/7000/E004 + deviceLabel: Temperature Humidity XS Sensor + manufacturerId: 0x027A + productType: 0x7000 + productId: 0xE004 + deviceProfileName: humidity-temperature-battery - id: "aeotec/multisensor/7" deviceLabel: Aeotec Multipurpose Sensor manufacturerId: 0x0371 From 63e87c3e87619a10a8ccb083e1d10fca933113db Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:40:00 -0600 Subject: [PATCH 448/449] Matter Camera: fix gating of audioRecording capability (#2820) audioRecording should only be included in the camera profile if the AUDIO feature of CAVSM is available and if the device supports the PushAvStreamTransport cluster (previously only the former was being checked). --- .../sub_drivers/camera/camera_utils/device_configuration.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 20641ebd33..4f467cbd07 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 @@ -72,7 +72,9 @@ function CameraDeviceConfiguration.match_profile(device, status_light_enabled_pr table.insert(main_component_capabilities, capabilities.localMediaStorage.ID) end if clus_has_feature(clusters.CameraAvStreamManagement.types.Feature.AUDIO) then - table.insert(main_component_capabilities, capabilities.audioRecording.ID) + if switch_utils.find_cluster_on_ep(camera_ep, clusters.PushAvStreamTransport.ID, "SERVER") then + table.insert(main_component_capabilities, capabilities.audioRecording.ID) + end table.insert(microphone_component_capabilities, capabilities.audioMute.ID) table.insert(microphone_component_capabilities, capabilities.audioVolume.ID) end From cf3b2caa73ec8f8bf07254bae3d47f1b6cf6dd6c Mon Sep 17 00:00:00 2001 From: Zhongpei Ge Date: Mon, 16 Mar 2026 19:34:44 +0800 Subject: [PATCH 449/449] knob for blind --- .../profiles/window-treatment-reverse.yml | 2 ++ .../tuya-zigbee/src/curtain/init.lua | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml index 9712eb6849..a6bbc35a10 100644 --- a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml +++ b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml @@ -4,6 +4,8 @@ components: capabilities: - id: windowShade version: 1 + - id: statelessSwitchLevelStep + version: 1 - id: windowShadeLevel version: 1 - id: windowShadePreset diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 03c34cac65..76446005d7 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -19,6 +19,7 @@ local device_management = require "st.zigbee.device_management" local tuya_utils = require "tuya_utils" local Basic = clusters.Basic local packet_id = 0 +local log = require "log" local PRESET_LEVEL = 50 local PRESET_LEVEL_KEY = "_presetLevel" @@ -109,6 +110,7 @@ local function window_shade_level(driver, device, command) if level > 100 then level = 100 end + log.info("capability handler level ------------->", level) level = utils.round(level) if device:get_manufacturer() == "_TZE284_nladmfvf" then level = 100 - level -- specific for _TZE284_nladmfvf @@ -153,6 +155,28 @@ local function tuya_cluster_handler(driver, device, zb_rx) end end +local function knob_to_window_shade_step_cmd(driver, device, command) + -- step1: get the rotateAmount + local step = command.args.stepSize + -- step2: get the current_level + local current_level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + -- calcultate the target_level + -- Tuya curtain devices use INVERTED position logic, + -- if we want to set to "target level" = "current_level" + "step" + -- we should send (100 - target level) to device + local target_level = 100-(current_level + step) + if target_level > 100 then + target_level = 100 + elseif target_level < 0 then + target_level = 0 + end + target_level = utils.round(target_level) + tuya_utils.send_tuya_command(device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00'..string.pack(">I2", target_level), packet_id) + packet_id = increase_packet_id(packet_id) + log.info("-------------------target_level", 100-target_level) + device:emit_event(capabilities.windowShadeLevel.shadeLevel(100-target_level)) +end + local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { @@ -173,6 +197,9 @@ local tuya_curtain_driver = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset, [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = set_preset_position_cmd + }, + [capabilities.statelessSwitchLevelStep.ID] = { + [capabilities.statelessSwitchLevelStep.commands.stepLevel.NAME] = knob_to_window_shade_step_cmd } }, zigbee_handlers = {